diff --git a/dashboard/admin.min.js b/dashboard/admin.min.js new file mode 100644 index 0000000..508f99b --- /dev/null +++ b/dashboard/admin.min.js @@ -0,0 +1 @@ +let currentPage={parts:1,aftermarket:1,crossref:1,fitment:1},categoriesCache=[],groupsCache=[],partsCache=[],manufacturersCache=[],brandsCache=[],pendingImportData=null,searchTimeout=null;const csvFormats={parts:{columns:["oem_part_number","name","name_es","group_id","description","description_es","weight_kg","material"],required:["oem_part_number","name","group_id"],example:"oem_part_number,name,name_es,group_id,description,description_es,weight_kg,material\n04465-33450,Brake Pad Set Front,Pastillas de Freno Delanteras,5,Front disc brake pads,Pastillas de freno de disco delanteras,1.2,Ceramic"},aftermarket:{columns:["oem_part_id","manufacturer_id","part_number","name","name_es","quality_tier","price_usd","warranty_months"],required:["oem_part_id","manufacturer_id","part_number"],example:"oem_part_id,manufacturer_id,part_number,name,name_es,quality_tier,price_usd,warranty_months\n1,3,BP-1234,Brake Pad Set,Pastillas de Freno,premium,45.99,24"},manufacturers:{columns:["name","type","quality_tier","country","website"],required:["name","type"],example:"name,type,quality_tier,country,website\nBrembo,aftermarket,premium,Italy,https://www.brembo.com"},categories:{columns:["name","name_es","slug","icon_name","display_order"],required:["name"],example:"name,name_es,slug,icon_name,display_order\nBrake System,Sistema de Frenos,brakes,brake,1"},groups:{columns:["category_id","name","name_es","display_order"],required:["category_id","name"],example:"category_id,name,name_es,display_order\n1,Brake Pads,Pastillas de Freno,1"},crossref:{columns:["part_id","cross_reference_number","reference_type","source","notes"],required:["part_id","cross_reference_number","reference_type"],example:"part_id,cross_reference_number,reference_type,source,notes\n1,D1210,interchange,Manufacturer,Compatible replacement"},fitment:{columns:["model_year_engine_id","part_id","quantity_required","position","fitment_notes"],required:["model_year_engine_id","part_id"],example:"model_year_engine_id,part_id,quantity_required,position,fitment_notes\n1,1,2,front,Fits all trims"}};function initSidebar(){const e=document.querySelectorAll(".sidebar-item");e.forEach((t=>{t.addEventListener("click",(()=>{showSection(t.dataset.section),e.forEach((e=>e.classList.remove("active"))),t.classList.add("active")}))}))}function showSection(e){document.querySelectorAll(".admin-section").forEach((e=>e.classList.remove("active")));const t=document.getElementById(`section-${e}`);if(t)switch(t.classList.add("active"),e){case"dashboard":loadDashboard();break;case"categories":loadCategories();break;case"groups":loadGroups();break;case"parts":loadParts();break;case"manufacturers":loadManufacturers();break;case"aftermarket":loadAftermarket();break;case"crossref":loadCrossRefs();break;case"fitment":loadFitment();break;case"diagrams":break;case"users":loadUsers()}document.querySelectorAll(".sidebar-item").forEach((t=>{t.classList.toggle("active",t.dataset.section===e)}))}function openModal(e){document.getElementById(e).classList.add("active")}function closeModal(e){document.getElementById(e).classList.remove("active")}function showAlert(e,t="success"){const a=document.getElementById("alertContainer"),n=document.createElement("div");n.className=`alert alert-${t}`,n.innerHTML=`${"success"===t?"✓":"✕"} ${e}`,a.appendChild(n),setTimeout((()=>n.remove()),5e3)}function debounceSearch(e){searchTimeout&&clearTimeout(searchTimeout),searchTimeout=setTimeout(e,300)}async function loadDashboard(){try{const e=await fetch("/api/admin/stats").then((e=>e.json()));document.getElementById("statCategories").textContent=e.categories||0,document.getElementById("statGroups").textContent=e.groups||0,document.getElementById("statParts").textContent=e.parts||0,document.getElementById("statAftermarket").textContent=e.aftermarket||0,document.getElementById("statManufacturers").textContent=e.manufacturers||0,document.getElementById("statFitment").textContent=e.fitment||0}catch(e){console.error("Error loading dashboard:",e)}}async function loadCategories(){try{const e=await fetch("/api/categories"),t=await e.json();categoriesCache=flattenCategories(t);const a=document.getElementById("categoriesTable");if(0===categoriesCache.length)return void(a.innerHTML='No hay categorías');a.innerHTML=categoriesCache.map((e=>`\n \n ${e.id}\n ${e.name}\n ${e.name_es||"-"}\n ${e.slug||"-"}\n ${e.icon_name||"-"}\n ${e.display_order||0}\n \n \n \n \n \n `)).join(""),updateCategorySelects()}catch(e){console.error("Error loading categories:",e),showAlert("Error al cargar categorías","error")}}function flattenCategories(e,t=0){let a=[];return e.forEach((e=>{a.push({...e,level:t}),e.children&&e.children.length>0&&(a=a.concat(flattenCategories(e.children,t+1)))})),a}function updateCategorySelects(){["groupCategoryFilter","groupCategory"].forEach((e=>{const t=document.getElementById(e);if(!t)return;const a=t.value,n=e.includes("Filter")?'':'';t.innerHTML=n+categoriesCache.map((e=>``)).join(""),t.value=a}))}function openCategoryModal(e=null){if(document.getElementById("categoryId").value="",document.getElementById("categoryName").value="",document.getElementById("categoryNameEs").value="",document.getElementById("categorySlug").value="",document.getElementById("categoryIcon").value="",document.getElementById("categoryOrder").value="0",document.getElementById("categoryModalTitle").textContent="Nueva Categoría",e){const t=categoriesCache.find((t=>t.id===e));t&&(document.getElementById("categoryId").value=t.id,document.getElementById("categoryName").value=t.name,document.getElementById("categoryNameEs").value=t.name_es||"",document.getElementById("categorySlug").value=t.slug||"",document.getElementById("categoryIcon").value=t.icon_name||"",document.getElementById("categoryOrder").value=t.display_order||0,document.getElementById("categoryModalTitle").textContent="Editar Categoría")}openModal("categoryModal")}function editCategory(e){openCategoryModal(e)}async function saveCategory(){const e=document.getElementById("categoryId").value,t={name:document.getElementById("categoryName").value,name_es:document.getElementById("categoryNameEs").value||null,slug:document.getElementById("categorySlug").value||null,icon_name:document.getElementById("categoryIcon").value||null,display_order:parseInt(document.getElementById("categoryOrder").value)||0};try{const a=e?`/api/admin/categories/${e}`:"/api/admin/categories",n=e?"PUT":"POST",r=await fetch(a,{method:n,headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!r.ok){const e=await r.json();throw new Error(e.error||"Error al guardar")}closeModal("categoryModal"),showAlert(e?"Categoría actualizada":"Categoría creada"),loadCategories()}catch(e){showAlert(e.message,"error")}}async function deleteCategory(e){if(confirm("¿Estás seguro de eliminar esta categoría?"))try{if(!(await fetch(`/api/admin/categories/${e}`,{method:"DELETE"})).ok)throw new Error("Error al eliminar");showAlert("Categoría eliminada"),loadCategories()}catch(e){showAlert(e.message,"error")}}async function loadGroups(){try{const e=document.getElementById("groupCategoryFilter").value;let t="/api/admin/groups";e&&(t+=`?category_id=${e}`);const a=await fetch(t),n=await a.json();groupsCache=n;const r=document.getElementById("groupsTable");if(0===n.length)return void(r.innerHTML='No hay grupos');r.innerHTML=n.map((e=>`\n \n ${e.id}\n ${e.name}\n ${e.name_es||"-"}\n ${e.category_name||"-"}\n ${e.display_order||0}\n \n \n \n \n \n `)).join(""),updateGroupSelects()}catch(e){console.error("Error loading groups:",e),showAlert("Error al cargar grupos","error")}}function updateGroupSelects(){["partGroupFilter","partGroup"].forEach((e=>{const t=document.getElementById(e);if(!t)return;const a=t.value,n=e.includes("Filter")?'':'';t.innerHTML=n+groupsCache.map((e=>``)).join(""),t.value=a}))}function openGroupModal(e=null){document.getElementById("groupId").value="",document.getElementById("groupName").value="",document.getElementById("groupNameEs").value="",document.getElementById("groupOrder").value="0",document.getElementById("groupModalTitle").textContent="Nuevo Grupo";if(document.getElementById("groupCategory").innerHTML=''+categoriesCache.map((e=>``)).join(""),e){const t=groupsCache.find((t=>t.id===e));t&&(document.getElementById("groupId").value=t.id,document.getElementById("groupName").value=t.name,document.getElementById("groupNameEs").value=t.name_es||"",document.getElementById("groupCategory").value=t.category_id,document.getElementById("groupOrder").value=t.display_order||0,document.getElementById("groupModalTitle").textContent="Editar Grupo")}openModal("groupModal")}function editGroup(e){openGroupModal(e)}async function saveGroup(){const e=document.getElementById("groupId").value,t={category_id:parseInt(document.getElementById("groupCategory").value),name:document.getElementById("groupName").value,name_es:document.getElementById("groupNameEs").value||null,display_order:parseInt(document.getElementById("groupOrder").value)||0};try{const a=e?`/api/admin/groups/${e}`:"/api/admin/groups",n=e?"PUT":"POST",r=await fetch(a,{method:n,headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!r.ok){const e=await r.json();throw new Error(e.error||"Error al guardar")}closeModal("groupModal"),showAlert(e?"Grupo actualizado":"Grupo creado"),loadGroups()}catch(e){showAlert(e.message,"error")}}async function deleteGroup(e){if(confirm("¿Estás seguro de eliminar este grupo?"))try{if(!(await fetch(`/api/admin/groups/${e}`,{method:"DELETE"})).ok)throw new Error("Error al eliminar");showAlert("Grupo eliminado"),loadGroups()}catch(e){showAlert(e.message,"error")}}async function loadParts(){try{const e=document.getElementById("partSearch").value,t=document.getElementById("partGroupFilter").value;let a=`/api/parts?page=${currentPage.parts}&per_page=20`;e&&(a+=`&search=${encodeURIComponent(e)}`),t&&(a+=`&group_id=${t}`);const n=await fetch(a),r=await n.json();partsCache=r.data||[];const o=document.getElementById("partsTable");if(0===partsCache.length)return o.innerHTML='No hay partes',void renderPagination("partsPagination",r.pagination,"parts",loadParts);o.innerHTML=partsCache.map((e=>`\n \n ${e.id}\n \n ${e.image_url?`${e.name}`:'📷'}\n \n ${e.oem_part_number}\n ${e.name}\n ${e.group_name||"-"}\n \n \n \n \n \n `)).join(""),renderPagination("partsPagination",r.pagination,"parts",loadParts),0===groupsCache.length&&await loadGroups()}catch(e){console.error("Error loading parts:",e),showAlert("Error al cargar partes","error")}}function openPartModal(e=null){document.getElementById("partId").value="",document.getElementById("partOemNumber").value="",document.getElementById("partName").value="",document.getElementById("partNameEs").value="",document.getElementById("partDescription").value="",document.getElementById("partDescriptionEs").value="",document.getElementById("partWeight").value="",document.getElementById("partMaterial").value="",document.getElementById("partModalTitle").textContent="Nueva Parte OEM",resetPartImagePreview();document.getElementById("partGroup").innerHTML=''+groupsCache.map((e=>``)).join(""),e&&fetchPartDetail(e),openModal("partModal")}async function fetchPartDetail(e){try{const t=await fetch(`/api/parts/${e}`),a=await t.json();document.getElementById("partId").value=a.id,document.getElementById("partOemNumber").value=a.oem_part_number,document.getElementById("partName").value=a.name,document.getElementById("partNameEs").value=a.name_es||"",document.getElementById("partGroup").value=a.group_id,document.getElementById("partDescription").value=a.description||"",document.getElementById("partDescriptionEs").value=a.description_es||"",document.getElementById("partModalTitle").textContent="Editar Parte OEM";const n=document.getElementById("partImagePreview");a.image_url?(n.innerHTML=`${a.name}`,document.getElementById("partImageUrl").value=a.image_url):resetPartImagePreview()}catch(e){console.error("Error fetching part:",e)}}function editPart(e){openPartModal(e)}async function savePart(){const e=document.getElementById("partId").value;let t=document.getElementById("partImageUrl").value||null;const a=document.getElementById("partImagePreview").querySelector("img");a&&a.src.startsWith("data:")&&(t=await uploadPartImage(a.src));const n={oem_part_number:document.getElementById("partOemNumber").value,name:document.getElementById("partName").value,name_es:document.getElementById("partNameEs").value||null,group_id:parseInt(document.getElementById("partGroup").value),description:document.getElementById("partDescription").value||null,description_es:document.getElementById("partDescriptionEs").value||null,weight_kg:parseFloat(document.getElementById("partWeight").value)||null,material:document.getElementById("partMaterial").value||null,image_url:t};try{const t=e?`/api/admin/parts/${e}`:"/api/admin/parts",a=e?"PUT":"POST",r=await fetch(t,{method:a,headers:{"Content-Type":"application/json"},body:JSON.stringify(n)});if(!r.ok){const e=await r.json();throw new Error(e.error||"Error al guardar")}closeModal("partModal"),showAlert(e?"Parte actualizada":"Parte creada"),loadParts()}catch(e){showAlert(e.message,"error")}}function previewPartImage(e){const t=document.getElementById("partImagePreview");if(e.files&&e.files[0]){const a=new FileReader;a.onload=function(e){t.innerHTML=`Preview`},a.readAsDataURL(e.files[0])}}async function uploadPartImage(e){try{const t=await fetch("/api/admin/upload-image",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({image:e})});if(!t.ok)throw new Error("Error uploading image");return(await t.json()).url}catch(e){return console.error("Image upload failed:",e),null}}function resetPartImagePreview(){document.getElementById("partImagePreview").innerHTML='📷 Sin imagen',document.getElementById("partImageUrl").value="",document.getElementById("partImageFile").value=""}async function deletePart(e){if(confirm("¿Estás seguro de eliminar esta parte?"))try{if(!(await fetch(`/api/admin/parts/${e}`,{method:"DELETE"})).ok)throw new Error("Error al eliminar");showAlert("Parte eliminada"),loadParts()}catch(e){showAlert(e.message,"error")}}async function loadManufacturers(){try{const e=await fetch("/api/manufacturers");manufacturersCache=await e.json();const t=document.getElementById("manufacturersTable");if(0===manufacturersCache.length)return void(t.innerHTML='No hay fabricantes');t.innerHTML=manufacturersCache.map((e=>`\n \n ${e.id}\n ${e.name}\n ${e.type||"-"}\n ${e.quality_tier||"-"}\n ${e.country||"-"}\n \n \n \n \n \n `)).join(""),updateManufacturerSelects()}catch(e){console.error("Error loading manufacturers:",e),showAlert("Error al cargar fabricantes","error")}}function updateManufacturerSelects(){const e=document.getElementById("aftermarketManufacturerFilter");if(e){const t=e.value;e.innerHTML=''+manufacturersCache.map((e=>``)).join(""),e.value=t}const t=document.getElementById("aftermarketManufacturer");t&&(t.innerHTML=''+manufacturersCache.map((e=>``)).join(""))}function openManufacturerModal(e=null){if(document.getElementById("manufacturerId").value="",document.getElementById("manufacturerName").value="",document.getElementById("manufacturerType").value="aftermarket",document.getElementById("manufacturerQuality").value="standard",document.getElementById("manufacturerCountry").value="",document.getElementById("manufacturerWebsite").value="",document.getElementById("manufacturerModalTitle").textContent="Nuevo Fabricante",e){const t=manufacturersCache.find((t=>t.id===e));t&&(document.getElementById("manufacturerId").value=t.id,document.getElementById("manufacturerName").value=t.name,document.getElementById("manufacturerType").value=t.type||"aftermarket",document.getElementById("manufacturerQuality").value=t.quality_tier||"standard",document.getElementById("manufacturerCountry").value=t.country||"",document.getElementById("manufacturerWebsite").value=t.website||"",document.getElementById("manufacturerModalTitle").textContent="Editar Fabricante")}openModal("manufacturerModal")}function editManufacturer(e){openManufacturerModal(e)}async function saveManufacturer(){const e=document.getElementById("manufacturerId").value,t={name:document.getElementById("manufacturerName").value,type:document.getElementById("manufacturerType").value,quality_tier:document.getElementById("manufacturerQuality").value,country:document.getElementById("manufacturerCountry").value||null,website:document.getElementById("manufacturerWebsite").value||null};try{const a=e?`/api/admin/manufacturers/${e}`:"/api/admin/manufacturers",n=e?"PUT":"POST",r=await fetch(a,{method:n,headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!r.ok){const e=await r.json();throw new Error(e.error||"Error al guardar")}closeModal("manufacturerModal"),showAlert(e?"Fabricante actualizado":"Fabricante creado"),loadManufacturers()}catch(e){showAlert(e.message,"error")}}async function deleteManufacturer(e){if(confirm("¿Estás seguro de eliminar este fabricante?"))try{if(!(await fetch(`/api/admin/manufacturers/${e}`,{method:"DELETE"})).ok)throw new Error("Error al eliminar");showAlert("Fabricante eliminado"),loadManufacturers()}catch(e){showAlert(e.message,"error")}}async function loadAftermarket(){try{const e=document.getElementById("aftermarketSearch").value,t=document.getElementById("aftermarketManufacturerFilter").value;let a=`/api/aftermarket?page=${currentPage.aftermarket}&per_page=20`;e&&(a+=`&search=${encodeURIComponent(e)}`),t&&(a+=`&manufacturer_id=${t}`);const n=await fetch(a),r=await n.json(),o=document.getElementById("aftermarketTable");if(!r.data||0===r.data.length)return o.innerHTML='No hay partes aftermarket',void renderPagination("aftermarketPagination",r.pagination,"aftermarket",loadAftermarket);o.innerHTML=r.data.map((e=>`\n \n ${e.id}\n ${e.part_number}\n ${e.name||"-"}\n ${e.oem_part_number}\n ${e.manufacturer_name}\n ${e.quality_tier||"-"}\n ${e.price_usd?"$"+e.price_usd.toFixed(2):"-"}\n \n \n \n \n \n `)).join(""),renderPagination("aftermarketPagination",r.pagination,"aftermarket",loadAftermarket)}catch(e){console.error("Error loading aftermarket:",e),showAlert("Error al cargar partes aftermarket","error")}}async function openAftermarketModal(e=null){document.getElementById("aftermarketId").value="",document.getElementById("aftermarketPartNumber").value="",document.getElementById("aftermarketName").value="",document.getElementById("aftermarketNameEs").value="",document.getElementById("aftermarketQuality").value="standard",document.getElementById("aftermarketPrice").value="",document.getElementById("aftermarketWarranty").value="",document.getElementById("aftermarketModalTitle").textContent="Nueva Parte Aftermarket",await loadPartsForSelect("aftermarketOemPart"),updateManufacturerSelects(),openModal("aftermarketModal")}async function editAftermarket(e){await openAftermarketModal(e);try{const t=await fetch("/api/aftermarket?search=&per_page=200"),a=await t.json(),n=(a.data||a).find((t=>t.id===e));n&&(document.getElementById("aftermarketId").value=n.id,document.getElementById("aftermarketPartNumber").value=n.part_number||"",document.getElementById("aftermarketName").value=n.name||"",document.getElementById("aftermarketNameEs").value=n.name_es||"",document.getElementById("aftermarketQuality").value=n.quality_tier||"standard",document.getElementById("aftermarketPrice").value=n.price_usd||"",document.getElementById("aftermarketWarranty").value=n.warranty_months||"",n.oem_part_id&&(document.getElementById("aftermarketOemPart").value=n.oem_part_id),n.manufacturer_id&&(document.getElementById("aftermarketManufacturer").value=n.manufacturer_id),document.getElementById("aftermarketModalTitle").textContent="Editar Parte Aftermarket")}catch(e){console.error("Error loading aftermarket for edit:",e)}}async function saveAftermarket(){const e=document.getElementById("aftermarketId").value,t={oem_part_id:parseInt(document.getElementById("aftermarketOemPart").value),manufacturer_id:parseInt(document.getElementById("aftermarketManufacturer").value),part_number:document.getElementById("aftermarketPartNumber").value,name:document.getElementById("aftermarketName").value||null,name_es:document.getElementById("aftermarketNameEs").value||null,quality_tier:document.getElementById("aftermarketQuality").value,price_usd:parseFloat(document.getElementById("aftermarketPrice").value)||null,warranty_months:parseInt(document.getElementById("aftermarketWarranty").value)||null};try{const a=e?`/api/admin/aftermarket/${e}`:"/api/admin/aftermarket",n=e?"PUT":"POST",r=await fetch(a,{method:n,headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!r.ok){const e=await r.json();throw new Error(e.error||"Error al guardar")}closeModal("aftermarketModal"),showAlert(e?"Parte actualizada":"Parte creada"),loadAftermarket()}catch(e){showAlert(e.message,"error")}}async function deleteAftermarket(e){if(confirm("¿Estás seguro de eliminar esta parte aftermarket?"))try{if(!(await fetch(`/api/admin/aftermarket/${e}`,{method:"DELETE"})).ok)throw new Error("Error al eliminar");showAlert("Parte eliminada"),loadAftermarket()}catch(e){showAlert(e.message,"error")}}async function loadCrossRefs(){try{const e=await fetch(`/api/admin/crossref?page=${currentPage.crossref}&per_page=20`),t=await e.json(),a=document.getElementById("crossrefTable");if(!t.data||0===t.data.length)return void(a.innerHTML='No hay cross-references');a.innerHTML=t.data.map((e=>`\n \n ${e.id}\n ${e.oem_part_number} - ${e.part_name}\n ${e.cross_reference_number}\n ${e.reference_type}\n ${e.source||"-"}\n \n \n \n \n \n `)).join(""),renderPagination("crossrefPagination",t.pagination,"crossref",loadCrossRefs)}catch(e){console.error("Error loading cross-refs:",e)}}async function openCrossRefModal(e=null){document.getElementById("crossrefId").value="",document.getElementById("crossrefNumber").value="",document.getElementById("crossrefType").value="interchange",document.getElementById("crossrefSource").value="",document.getElementById("crossrefNotes").value="",document.getElementById("crossrefModalTitle").textContent="Nueva Cross-Reference",await loadPartsForSelect("crossrefPart"),openModal("crossrefModal")}function editCrossRef(e){openCrossRefModal(e)}async function saveCrossRef(){const e=document.getElementById("crossrefId").value,t={part_id:parseInt(document.getElementById("crossrefPart").value),cross_reference_number:document.getElementById("crossrefNumber").value,reference_type:document.getElementById("crossrefType").value,source:document.getElementById("crossrefSource").value||null,notes:document.getElementById("crossrefNotes").value||null};try{const a=e?`/api/admin/crossref/${e}`:"/api/admin/crossref",n=e?"PUT":"POST",r=await fetch(a,{method:n,headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!r.ok){const e=await r.json();throw new Error(e.error||"Error al guardar")}closeModal("crossrefModal"),showAlert(e?"Referencia actualizada":"Referencia creada"),loadCrossRefs()}catch(e){showAlert(e.message,"error")}}async function deleteCrossRef(e){if(confirm("¿Estás seguro de eliminar esta referencia?"))try{if(!(await fetch(`/api/admin/crossref/${e}`,{method:"DELETE"})).ok)throw new Error("Error al eliminar");showAlert("Referencia eliminada"),loadCrossRefs()}catch(e){showAlert(e.message,"error")}}async function loadFitment(){try{const e=document.getElementById("fitmentBrandFilter").value,t=document.getElementById("fitmentModelFilter").value;let a=`/api/admin/fitment?page=${currentPage.fitment}&per_page=20`;e&&(a+=`&brand=${encodeURIComponent(e)}`),t&&(a+=`&model=${encodeURIComponent(t)}`);const n=await fetch(a),r=await n.json(),o=document.getElementById("fitmentTable");if(!r.data||0===r.data.length)return o.innerHTML='No hay fitments',void renderPagination("fitmentPagination",r.pagination,"fitment",loadFitment);o.innerHTML=r.data.map((e=>`\n \n ${e.id}\n ${e.brand} ${e.model} ${e.year} - ${e.engine}\n ${e.oem_part_number} - ${e.part_name}\n ${e.quantity_required}\n ${e.position||"-"}\n \n \n \n \n `)).join(""),renderPagination("fitmentPagination",r.pagination,"fitment",loadFitment)}catch(e){console.error("Error loading fitment:",e)}}async function loadFitmentModels(){const e=document.getElementById("fitmentBrandFilter").value,t=document.getElementById("fitmentModelFilter");if(!e)return t.innerHTML='',void loadFitment();try{const a=await fetch(`/api/models?brand=${encodeURIComponent(e)}`),n=await a.json();t.innerHTML=''+n.map((e=>``)).join(""),loadFitment()}catch(e){console.error("Error loading models:",e)}}async function openFitmentModal(){document.getElementById("fitmentId").value="",document.getElementById("fitmentQuantity").value="1",document.getElementById("fitmentPosition").value="",document.getElementById("fitmentNotes").value="",document.getElementById("fitmentModalTitle").textContent="Nuevo Fitment",await loadVehiclesForSelect("fitmentVehicle"),await loadPartsForSelect("fitmentPart"),openModal("fitmentModal")}async function saveFitment(){const e=document.getElementById("fitmentId").value,t={model_year_engine_id:parseInt(document.getElementById("fitmentVehicle").value),part_id:parseInt(document.getElementById("fitmentPart").value),quantity_required:parseInt(document.getElementById("fitmentQuantity").value)||1,position:document.getElementById("fitmentPosition").value||null,fitment_notes:document.getElementById("fitmentNotes").value||null};try{const a=e?`/api/admin/fitment/${e}`:"/api/admin/fitment",n=e?"PUT":"POST",r=await fetch(a,{method:n,headers:{"Content-Type":"application/json"},body:JSON.stringify(t)});if(!r.ok){const e=await r.json();throw new Error(e.error||"Error al guardar")}closeModal("fitmentModal"),showAlert(e?"Fitment actualizado":"Fitment creado"),loadFitment()}catch(e){showAlert(e.message,"error")}}async function deleteFitment(e){if(confirm("¿Estás seguro de eliminar este fitment?"))try{if(!(await fetch(`/api/admin/fitment/${e}`,{method:"DELETE"})).ok)throw new Error("Error al eliminar");showAlert("Fitment eliminado"),loadFitment()}catch(e){showAlert(e.message,"error")}}async function loadPartsForSelect(e){const t=document.getElementById(e);if(t)try{const e=await fetch("/api/parts?per_page=100"),a=await e.json();t.innerHTML=''+(a.data||[]).map((e=>``)).join("")}catch(e){console.error("Error loading parts for select:",e)}}async function loadVehiclesForSelect(e){const t=document.getElementById(e);if(t)try{const e=await fetch("/api/model-year-engine?per_page=100"),a=await e.json(),n=a.data||a;t.innerHTML=''+n.map((e=>``)).join("")}catch(e){console.error("Error loading vehicles for select:",e)}}async function loadBrands(){try{const e=await fetch("/api/brands");brandsCache=await e.json();const t=document.getElementById("fitmentBrandFilter");t&&(t.innerHTML=''+brandsCache.map((e=>``)).join(""))}catch(e){console.error("Error loading brands:",e)}}function renderPagination(e,t,a,n){const r=document.getElementById(e);if(!r||!t)return void(r.innerHTML="");const{page:o,total_pages:i}=t;if(i<=1)return void(r.innerHTML="");let d="";d+=``;const l=Math.max(1,o-2),c=Math.min(i,o+2);for(let e=l;e<=c;e++)d+=``;d+=``,r.innerHTML=d}function goToPage(e,t,a){currentPage[e]=t,window[a]()}function initDropZone(){const e=document.getElementById("dropZone"),t=document.getElementById("csvFile");e.addEventListener("click",(()=>t.click())),e.addEventListener("dragover",(t=>{t.preventDefault(),e.classList.add("dragover")})),e.addEventListener("dragleave",(()=>{e.classList.remove("dragover")})),e.addEventListener("drop",(t=>{t.preventDefault(),e.classList.remove("dragover");const a=t.dataTransfer.files[0];a&&a.name.endsWith(".csv")?handleCsvFile(a):showAlert("Por favor selecciona un archivo CSV","error")})),t.addEventListener("change",(e=>{const t=e.target.files[0];t&&handleCsvFile(t)}))}function initImportTypeChange(){document.getElementById("importType").addEventListener("change",updateCsvFormatHelp),updateCsvFormatHelp()}function updateCsvFormatHelp(){const e=document.getElementById("importType").value,t=csvFormats[e],a=document.getElementById("csvFormatHelp");t&&(a.innerHTML=`\n

Columnas requeridas: ${t.required.join(", ")}

\n

Todas las columnas: ${t.columns.join(", ")}

\n

Ejemplo:

\n
${t.example}
\n `)}function handleCsvFile(e){const t=new FileReader;t.onload=e=>{const t=parseCSV(e.target.result);t.length<2?showAlert("El archivo CSV debe tener al menos un encabezado y una fila de datos","error"):(pendingImportData=t,showImportPreview(t))},t.readAsText(e)}function parseCSV(e){const t=e.split(/\r?\n/).filter((e=>e.trim())),a=[];for(const e of t){const t=[];let n="",r=!1;for(let a=0;a"+t.map((e=>`${e}`)).join("")+"",document.getElementById("previewBody").innerHTML=a.map((e=>""+e.map((e=>`${e}`)).join("")+"")).join(""),document.getElementById("importPreview").style.display="block"}function cancelImport(){pendingImportData=null,document.getElementById("importPreview").style.display="none",document.getElementById("csvFile").value=""}async function executeImport(){if(!pendingImportData)return;const e=document.getElementById("importType").value,t=csvFormats[e],a=pendingImportData[0],n=pendingImportData.slice(1);for(const e of t.required)if(!a.includes(e))return void showAlert(`Falta columna requerida: ${e}`,"error");const r=n.map((e=>{const t={};return a.forEach(((a,n)=>{let r=e[n]||null;["id","category_id","group_id","part_id","oem_part_id","manufacturer_id","model_year_engine_id","display_order","quantity_required","warranty_months"].includes(a)?r=r?parseInt(r):null:["weight_kg","price_usd"].includes(a)&&(r=r?parseFloat(r):null),t[a]=r})),t}));try{const t=await fetch(`/api/admin/import/${e}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({records:r})}),a=await t.json();if(!t.ok)throw new Error(a.error||"Error en la importación");showAlert(`Importados ${a.imported} registros exitosamente`),cancelImport(),loadDashboard()}catch(e){showAlert(e.message,"error")}}async function exportData(e){try{const t=await fetch(`/api/admin/export/${e}`),a=await t.json();if(!a.data||0===a.data.length)return void showAlert("No hay datos para exportar","error");const n=Object.keys(a.data[0]);let r=n.join(",")+"\n";for(const e of a.data)r+=n.map((t=>{let a=e[t];return null==a?"":(a=String(a),a.includes(",")||a.includes('"')||a.includes("\n")?'"'+a.replace(/"/g,'""')+'"':a)})).join(",")+"\n";const o=new Blob([r],{type:"text/csv;charset=utf-8;"}),i=document.createElement("a");i.href=URL.createObjectURL(o),i.download=`${e}_export_${(new Date).toISOString().slice(0,10)}.csv`,i.click(),showAlert(`Exportados ${a.data.length} registros`)}catch(e){showAlert("Error al exportar: "+e.message,"error")}}document.addEventListener("DOMContentLoaded",(()=>{initSidebar(),initDropZone(),initImportTypeChange(),loadDashboard()})),loadBrands();let bulkSelectedMYEId=null,bulkAvailableParts=[];async function initBulkEditor(){const e=document.getElementById("bulkBrand");e&&brandsCache.length>0&&(e.innerHTML=''+brandsCache.map((e=>``)).join("")),0===categoriesCache.length&&await loadCategories();const t=document.getElementById("bulkCategory");t&&(t.innerHTML=''+categoriesCache.map((e=>``)).join(""))}async function loadBulkModels(){const e=document.getElementById("bulkBrand").value,t=document.getElementById("bulkModel"),a=document.getElementById("bulkYear"),n=document.getElementById("bulkEngine");if(t.innerHTML='',a.innerHTML='',n.innerHTML='',document.getElementById("bulkVehicleSelected").style.display="none",e)try{const a=await fetch(`/api/models?brand=${encodeURIComponent(e)}`),n=await a.json();t.innerHTML=''+n.map((e=>``)).join("")}catch(e){console.error("Error loading models:",e)}}async function loadBulkYears(){const e=document.getElementById("bulkBrand").value,t=document.getElementById("bulkModel").value,a=document.getElementById("bulkYear"),n=document.getElementById("bulkEngine");if(a.innerHTML='',n.innerHTML='',document.getElementById("bulkVehicleSelected").style.display="none",e&&t)try{const n=await fetch(`/api/years?brand=${encodeURIComponent(e)}&model=${encodeURIComponent(t)}`),r=await n.json();a.innerHTML=''+r.map((e=>``)).join("")}catch(e){console.error("Error loading years:",e)}}async function loadBulkEngines(){const e=document.getElementById("bulkBrand").value,t=document.getElementById("bulkModel").value,a=document.getElementById("bulkYear").value,n=document.getElementById("bulkEngine");if(n.innerHTML='',document.getElementById("bulkVehicleSelected").style.display="none",e&&t&&a)try{const r=await fetch(`/api/engines?brand=${encodeURIComponent(e)}&model=${encodeURIComponent(t)}&year=${a}`),o=(await r.json(),await fetch(`/api/model-year-engine?brand=${encodeURIComponent(e)}&model=${encodeURIComponent(t)}&year=${a}&per_page=100`)),i=await o.json(),d=i.data||i;n.innerHTML=''+d.map((e=>``)).join("")}catch(e){console.error("Error loading engines:",e)}}async function selectBulkVehicle(){const e=document.getElementById("bulkEngine").value;if(!e)return void(document.getElementById("bulkVehicleSelected").style.display="none");bulkSelectedMYEId=parseInt(e);const t=document.getElementById("bulkBrand").value,a=document.getElementById("bulkModel").value,n=document.getElementById("bulkYear").value,r=document.getElementById("bulkEngine").options[document.getElementById("bulkEngine").selectedIndex].text;document.getElementById("bulkVehicleName").textContent=`${t} ${a} ${n} - ${r}`,document.getElementById("bulkMYEId").textContent=e,document.getElementById("bulkVehicleSelected").style.display="block",await initBulkEditor(),await loadBulkParts()}async function loadBulkParts(){const e=document.getElementById("bulkPartsContainer"),t=document.getElementById("bulkCategory").value;e.innerHTML='

Cargando partes...

';try{let a="/api/parts?per_page=100";t&&(a+=`&category_id=${t}`);const n=await fetch(a),r=await n.json();if(bulkAvailableParts=r.data||[],0===bulkAvailableParts.length)return void(e.innerHTML='

No hay partes disponibles en esta categoría.

');const o=await fetch(`/api/admin/fitment?mye_id=${bulkSelectedMYEId}&per_page=500`),i=await o.json(),d=new Set((i.data||[]).map((e=>e.part_id)));e.innerHTML=bulkAvailableParts.map((e=>`\n
\n \n
\n
${e.oem_part_number}
\n
${e.name}
\n
${e.group_name||""} · ${e.category_name||""}
\n
\n \n
\n `)).join(""),updateBulkSelectedCount()}catch(t){console.error("Error loading parts:",t),e.innerHTML='

Error al cargar partes

'}}function toggleBulkPart(e,t){const a=e.querySelector('input[type="checkbox"]');a.disabled||(a.checked=!a.checked,e.classList.toggle("selected",a.checked),updateBulkSelectedCount())}function updateBulkSelectedCount(){const e=document.querySelectorAll('#bulkPartsContainer input[type="checkbox"]:checked:not(:disabled)');document.getElementById("bulkSelectedCount").textContent=`${e.length} partes seleccionadas`}async function saveBulkFitments(){if(!bulkSelectedMYEId)return void showAlert("Selecciona un vehículo primero","error");const e=document.querySelectorAll("#bulkPartsContainer .bulk-part-item"),t=[];if(e.forEach((e=>{const a=e.querySelector('input[type="checkbox"]');if(a.checked&&!a.disabled){const n=parseInt(a.dataset.partId),r=parseInt(e.querySelector(".bulk-part-qty").value)||1;t.push({model_year_engine_id:bulkSelectedMYEId,part_id:n,quantity_required:r})}})),0!==t.length)try{const e=await fetch("/api/admin/import/fitment",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({records:t})}),a=await e.json();if(!e.ok)throw new Error(a.error||"Error al guardar");showAlert(`${a.imported} fitments creados exitosamente`),await loadBulkParts(),loadFitment(),loadDashboard()}catch(e){showAlert(e.message,"error")}else showAlert("Selecciona al menos una parte","error")}const originalShowSection=showSection;showSection=function(e){originalShowSection(e),"fitment"===e&&initBulkEditor()};let currentEditorDiagramId=null,currentEditorHotspots=[],partSearchTimeout=null;async function searchDiagramsAdmin(){const e=document.getElementById("diagramSearchInput").value.trim(),t=document.getElementById("diagramSearchResults");if(e){t.innerHTML='

Buscando...

';try{const a=await fetch(`/api/diagrams/search?q=${encodeURIComponent(e)}`),n=await a.json();if(0===n.length)return void(t.innerHTML='

No se encontraron diagramas

');t.innerHTML=n.map((e=>{const t=e.image_path?"/"+e.image_path:`/static/diagrams/moog/${e.name}.jpg`;return`\n
\n ${e.name}\n
\n
${e.name}
\n
${e.name_es||e.source||""}
\n
\n
`})).join("")}catch(e){t.innerHTML='

Error al buscar diagramas

'}}else t.innerHTML='

Ingresa un código de diagrama para buscar

'}async function openHotspotEditor(e){currentEditorDiagramId=e,document.getElementById("hotspotEditorArea").style.display="block";try{const t=await fetch(`/api/diagrams/${e}`),a=await t.json();document.getElementById("hotspotEditorTitle").textContent=`${a.name} - ${a.name_es||a.group_name||""}`;const n=a.image_url||(a.image_path?"/"+a.image_path:"");document.getElementById("hotspotEditorImg").src=n,currentEditorHotspots=a.hotspots||[],renderEditorHotspots(),clearHotspotForm();const r=currentEditorHotspots.reduce(((e,t)=>Math.max(e,t.callout_number||0)),0);document.getElementById("hsCallout").value=r+1,document.getElementById("hotspotEditorArea").scrollIntoView({behavior:"smooth"})}catch(e){showAlert("Error al cargar diagrama","error")}}function closeHotspotEditor(){document.getElementById("hotspotEditorArea").style.display="none",currentEditorDiagramId=null,currentEditorHotspots=[]}function onHotspotImageClick(e){const t=e.target.getBoundingClientRect(),a=((e.clientX-t.left)/t.width*100).toFixed(2),n=((e.clientY-t.top)/t.height*100).toFixed(2);document.getElementById("hsCoords").value=`${a},${n}`,renderEditorHotspots();const r=document.getElementById("hotspotMarkersContainer"),o=document.createElement("div");o.style.cssText=`position:absolute;left:${a}%;top:${n}%;width:24px;height:24px;border-radius:50%;background:rgba(46,204,113,0.5);border:2px solid #2ecc71;transform:translate(-50%,-50%);pointer-events:none;z-index:10`,r.appendChild(o)}function renderEditorHotspots(){const e=document.getElementById("hotspotMarkersContainer"),t=document.getElementById("hotspotsList");e.innerHTML=currentEditorHotspots.map((e=>{const t=(e.coords||"").split(",");return t.length<2?"":`
${e.callout_number||""}
`})).join(""),0!==currentEditorHotspots.length?t.innerHTML=currentEditorHotspots.map((e=>`\n
\n ${e.callout_number||"?"}\n
\n
${e.part_name||e.label||"Sin parte"}
\n
${e.part_number||""} | ${e.coords}
\n
\n \n \n
\n `)).join(""):t.innerHTML='

No hay hotspots

'}function editHotspot(e){const t=currentEditorHotspots.find((t=>t.id===e));t&&(document.getElementById("hsEditId").value=t.id,document.getElementById("hsCoords").value=t.coords||"",document.getElementById("hsCallout").value=t.callout_number||"",document.getElementById("hsLabel").value=t.label||"",document.getElementById("hsPartId").value=t.part_id||"",document.getElementById("hsPartSearch").value=t.part_name?`${t.part_number} - ${t.part_name}`:"")}function clearHotspotForm(){document.getElementById("hsEditId").value="",document.getElementById("hsCoords").value="",document.getElementById("hsLabel").value="",document.getElementById("hsPartId").value="",document.getElementById("hsPartSearch").value="",document.getElementById("hsPartSelect").style.display="none";const e=currentEditorHotspots.reduce(((e,t)=>Math.max(e,t.callout_number||0)),0);document.getElementById("hsCallout").value=e+1}async function searchPartsForHotspot(e){clearTimeout(partSearchTimeout);const t=document.getElementById("hsPartSelect");!e||e.length<2?t.style.display="none":partSearchTimeout=setTimeout((async()=>{try{const a=await fetch(`/api/parts?search=${encodeURIComponent(e)}&per_page=20`),n=await a.json(),r=n.data||n;0===r.length?t.innerHTML="":t.innerHTML=r.map((e=>``)).join(""),t.style.display="block",t.onchange=function(){const e=t.options[t.selectedIndex];document.getElementById("hsPartId").value=e.value,document.getElementById("hsPartSearch").value=e.textContent,t.style.display="none"}}catch(e){t.innerHTML="",t.style.display="block"}}),300)}async function saveHotspot(){const e=document.getElementById("hsEditId").value,t=document.getElementById("hsCoords").value.trim(),a=parseInt(document.getElementById("hsCallout").value)||null,n=parseInt(document.getElementById("hsPartId").value)||null,r=document.getElementById("hsLabel").value.trim();if(!t)return void showAlert("Haz clic en la imagen para seleccionar posición","error");const o={diagram_id:currentEditorDiagramId,coords:t,callout_number:a,part_id:n,label:r,shape:"circle",color:"#e74c3c"};try{let t;t=e?await fetch(`/api/admin/hotspots/${e}`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)}):await fetch("/api/admin/hotspots",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)});const a=await t.json();if(!t.ok)throw new Error(a.error||"Error al guardar");showAlert(e?"Hotspot actualizado":"Hotspot creado"),await openHotspotEditor(currentEditorDiagramId)}catch(e){showAlert(e.message,"error")}}async function deleteHotspot(e){if(confirm("Eliminar este hotspot?"))try{const t=await fetch(`/api/admin/hotspots/${e}`,{method:"DELETE"}),a=await t.json();if(!t.ok)throw new Error(a.error||"Error al eliminar");showAlert("Hotspot eliminado"),await openHotspotEditor(currentEditorDiagramId)}catch(e){showAlert(e.message,"error")}}const roleBadgeColors={ADMIN:"#3b82f6",OWNER:"#8b5cf6",TALLER:"#22c55e",BODEGA:"#f59e0b"};function formatDate(e){if(!e)return'Nunca';var t=new Date(e);return isNaN(t.getTime())?e:t.toLocaleDateString("es-MX",{year:"numeric",month:"short",day:"numeric"})+" "+t.toLocaleTimeString("es-MX",{hour:"2-digit",minute:"2-digit"})}function getRoleBadge(e){return''+(e||"N/A")+""}function getActiveBadge(e){return e?'Activo':'Inactivo'}async function loadUsers(){var e=localStorage.getItem("access_token"),t=document.getElementById("usersTable");t.innerHTML='
';try{var a=await fetch("/api/admin/users",{headers:{Authorization:"Bearer "+e}});if(!a.ok)throw new Error("Error al cargar usuarios ("+a.status+")");var n=await a.json(),r=Array.isArray(n)?n:n.data||[],o=r.filter((function(e){return!e.is_active})).length,i=document.getElementById("pendingUsersBadge");if(i&&(o>0?(i.textContent=o,i.style.display="inline-block"):i.style.display="none"),0===r.length)return void(t.innerHTML='No hay usuarios registrados');t.innerHTML=r.map((function(e){var t=e.is_active?"Desactivar":"Activar",a=e.is_active?"btn-secondary":"btn-primary";return""+(e.name||e.nombre||"-")+""+(e.email||"-")+""+(e.business_name||e.negocio||"-")+""+getRoleBadge(e.role||e.rol)+""+getActiveBadge(e.is_active)+""+formatDate(e.last_login||e.ultimo_login)+'"})).join("")}catch(e){t.innerHTML=''+e.message+""}}async function toggleUserActive(e,t){var a=localStorage.getItem("access_token");if(confirm("¿Seguro que deseas "+(t?"desactivar":"activar")+" este usuario?"))try{var n=await fetch("/api/admin/users/"+e+"/activate",{method:"PUT",headers:{"Content-Type":"application/json",Authorization:"Bearer "+a},body:JSON.stringify({is_active:!t})});if(!n.ok){var r=await n.json();throw new Error(r.error||"Error al actualizar usuario")}showAlert("Usuario "+(t?"desactivado":"activado")+" correctamente"),loadUsers()}catch(e){showAlert(e.message,"error")}} \ No newline at end of file diff --git a/dashboard/bodega.min.css b/dashboard/bodega.min.css new file mode 100644 index 0000000..3626c73 --- /dev/null +++ b/dashboard/bodega.min.css @@ -0,0 +1,485 @@ +/* ============================================================ + bodega.css -- Styles for Nexus Autoparts Warehouse (Bodega) + ============================================================ */ + +/* --- Layout --- */ +.bodega-container { + max-width: 1100px; + margin: 0 auto; + padding: 5.5rem 2rem 3rem; +} + +/* --- Tabs --- */ +.bodega-tabs { + display: flex; + gap: 0; + border-bottom: 2px solid var(--border); + margin-bottom: 1.5rem; +} + +.bodega-tab { + padding: 0.8rem 1.8rem; + background: transparent; + border: none; + color: var(--text-secondary); + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + border-bottom: 3px solid transparent; + transition: all 0.2s; + position: relative; + bottom: -2px; +} + +.bodega-tab:hover { + color: var(--text-primary); + background: var(--bg-hover); +} + +.bodega-tab.active { + color: var(--accent); + border-bottom-color: var(--accent); +} + +.bodega-section { + display: none; +} + +.bodega-section.active { + display: block; +} + +/* --- Section Intro --- */ +.section-intro { + margin-bottom: 1.5rem; +} + +.section-intro h2 { + font-size: 1.3rem; + font-weight: 700; + margin-bottom: 0.5rem; +} + +.section-intro p { + color: var(--text-secondary); + font-size: 0.9rem; + line-height: 1.5; +} + +/* --- Mapping Form --- */ +.mapping-form { + max-width: 550px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 12px; + padding: 1.5rem; +} + +.mapping-form .form-group { + margin-bottom: 1.25rem; +} + +.mapping-form .form-label { + display: block; + margin-bottom: 0.4rem; + font-weight: 500; + font-size: 0.9rem; + color: var(--text-primary); +} + +.required { + color: var(--danger); + font-weight: 700; +} + +.optional { + color: var(--text-secondary); + font-size: 0.8rem; + font-weight: 400; +} + +.form-hint { + display: block; + margin-top: 0.3rem; + font-size: 0.75rem; + color: var(--text-secondary); +} + +.form-actions { + display: flex; + align-items: center; + gap: 1rem; + margin-top: 1.25rem; +} + +.status-msg { + font-size: 0.85rem; + font-weight: 500; +} + +.status-msg.success { + color: var(--success); +} + +.status-msg.error { + color: var(--danger); +} + +/* --- Upload Zone --- */ +.upload-zone { + border: 2px dashed var(--border); + border-radius: 12px; + padding: 3rem 2rem; + text-align: center; + cursor: pointer; + transition: all 0.3s; + background: var(--bg-card); + margin-bottom: 1rem; +} + +.upload-zone:hover, +.upload-zone.dragover { + border-color: var(--accent); + background: rgba(255, 107, 53, 0.05); +} + +.upload-icon { + font-size: 3rem; + margin-bottom: 0.75rem; +} + +.upload-text { + font-size: 1rem; + font-weight: 500; + margin-bottom: 0.3rem; +} + +.upload-hint { + font-size: 0.8rem; + color: var(--text-secondary); +} + +/* --- Selected File --- */ +.selected-file { + display: flex; + align-items: center; + gap: 0.75rem; + background: var(--bg-card); + border: 1px solid var(--accent); + border-radius: 8px; + padding: 0.6rem 1rem; + margin-bottom: 1rem; + font-size: 0.9rem; +} + +.btn-icon { + background: none; + border: none; + color: var(--text-secondary); + font-size: 1.2rem; + cursor: pointer; + padding: 0.1rem 0.3rem; + line-height: 1; + transition: color 0.2s; +} + +.btn-icon:hover { + color: var(--danger); +} + +/* --- Upload Result --- */ +.upload-result { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + padding: 1.25rem; + margin-bottom: 1.5rem; +} + +.upload-result h4 { + margin-bottom: 0.75rem; + font-size: 1rem; +} + +.result-stats { + display: flex; + gap: 1.5rem; + margin-bottom: 0.75rem; +} + +.result-stat { + display: flex; + align-items: center; + gap: 0.4rem; + font-size: 0.9rem; +} + +.result-stat.ok { + color: var(--success); +} + +.result-stat.err { + color: var(--danger); +} + +.error-samples { + margin-top: 0.75rem; + padding: 0.75rem; + background: var(--bg-secondary); + border-radius: 8px; + font-size: 0.8rem; + color: var(--text-secondary); + max-height: 150px; + overflow-y: auto; +} + +.error-samples p { + margin-bottom: 0.3rem; +} + +/* --- History --- */ +.history-section { + margin-top: 2rem; +} + +.history-section h3 { + font-size: 1rem; + font-weight: 600; + margin-bottom: 1rem; +} + +/* --- Tables --- */ +.table-wrap { + overflow-x: auto; +} + +.data-table { + width: 100%; + border-collapse: collapse; + font-size: 0.88rem; +} + +.data-table thead th { + background: var(--bg-secondary); + color: var(--text-secondary); + text-transform: uppercase; + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.05em; + padding: 0.75rem 1rem; + text-align: left; + border-bottom: 1px solid var(--border); + white-space: nowrap; +} + +.data-table tbody td { + padding: 0.7rem 1rem; + border-bottom: 1px solid var(--border); + color: var(--text-primary); +} + +.data-table tbody tr:hover { + background: var(--bg-hover); +} + +.empty-row { + text-align: center; + color: var(--text-secondary); + padding: 2rem 1rem !important; +} + +/* --- Status Badges --- */ +.badge { + display: inline-block; + padding: 0.2rem 0.6rem; + border-radius: 10px; + font-size: 0.72rem; + font-weight: 600; + text-transform: uppercase; +} + +.badge-success { + background: rgba(34, 197, 94, 0.15); + color: var(--success); +} + +.badge-error { + background: rgba(255, 68, 68, 0.15); + color: var(--danger); +} + +.badge-pending { + background: rgba(245, 158, 11, 0.15); + color: var(--warning); +} + +.badge-processing { + background: rgba(59, 130, 246, 0.15); + color: var(--info); +} + +/* --- Inventory Toolbar --- */ +.inventory-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; + flex-wrap: wrap; +} + +.search-box { + display: flex; + gap: 0.5rem; + flex: 1; + max-width: 500px; +} + +.search-box .form-input { + flex: 1; +} + +.btn-danger { + background: rgba(255, 68, 68, 0.15); + border: 1px solid var(--danger); + color: var(--danger); + padding: 0.7rem 1.5rem; + border-radius: 10px; + font-weight: 600; + cursor: pointer; + font-size: 0.9rem; + transition: all 0.3s; +} + +.btn-danger:hover { + background: var(--danger); + color: white; +} + +/* --- Pagination --- */ +.bodega-pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 0.5rem; + margin-top: 1.25rem; + flex-wrap: wrap; +} + +.bodega-pagination button { + padding: 0.4rem 0.8rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-secondary); + cursor: pointer; + font-size: 0.85rem; + transition: all 0.2s; +} + +.bodega-pagination button:hover:not(:disabled) { + border-color: var(--accent); + color: var(--accent); +} + +.bodega-pagination button.active { + background: var(--accent); + border-color: var(--accent); + color: white; +} + +.bodega-pagination button:disabled { + opacity: 0.4; + cursor: default; +} + +/* --- Confirm Modal --- */ +.confirm-box { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 14px; + padding: 2rem; + max-width: 420px; + width: 100%; +} + +.confirm-box h3 { + margin-bottom: 0.75rem; + font-size: 1.1rem; +} + +.confirm-box p { + color: var(--text-secondary); + font-size: 0.9rem; + margin-bottom: 1.5rem; + line-height: 1.5; +} + +.confirm-actions { + display: flex; + justify-content: flex-end; + gap: 0.75rem; +} + +/* --- Toast --- */ +#toast-container { + position: fixed; + bottom: 2rem; + right: 2rem; + z-index: 3000; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.toast { + padding: 0.8rem 1.2rem; + border-radius: 8px; + font-size: 0.85rem; + font-weight: 500; + animation: fadeIn 0.3s ease; + max-width: 350px; +} + +.toast.success { + background: rgba(34, 197, 94, 0.15); + border: 1px solid var(--success); + color: var(--success); +} + +.toast.error { + background: rgba(255, 68, 68, 0.15); + border: 1px solid var(--danger); + color: var(--danger); +} + +/* --- Responsive --- */ +@media (max-width: 768px) { + .bodega-container { + padding: 5rem 1rem 2rem; + } + + .bodega-tabs { + overflow-x: auto; + } + + .bodega-tab { + padding: 0.7rem 1.2rem; + font-size: 0.85rem; + white-space: nowrap; + } + + .inventory-toolbar { + flex-direction: column; + align-items: stretch; + } + + .search-box { + max-width: none; + } + + .result-stats { + flex-direction: column; + gap: 0.5rem; + } +} diff --git a/dashboard/bodega.min.js b/dashboard/bodega.min.js new file mode 100644 index 0000000..64ebd1b --- /dev/null +++ b/dashboard/bodega.min.js @@ -0,0 +1 @@ +!function(){"use strict";var e=null,t=1,n="";function r(){return localStorage.getItem("access_token")||""}function o(e){var t={Authorization:"Bearer "+r()};if(e)for(var n in e)t[n]=e[n];return t}function a(){var e=localStorage.getItem("refresh_token");return e?fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refresh_token:e})}).then((function(e){if(!e.ok)throw new Error("Refresh failed");return e.json()})).then((function(e){return e.access_token&&localStorage.setItem("access_token",e.access_token),e})):Promise.reject(new Error("No refresh token"))}function i(e,t){return(t=t||{}).headers||(t.headers={}),t.headers.Authorization="Bearer "+r(),fetch(""+e,t).then((function(n){return 401===n.status?a().then((function(){return t.headers.Authorization="Bearer "+r(),fetch(""+e,t)})).then((function(e){return e.ok?e.json():e.json().then((function(e){throw new Error(e.error||"Error")}))})):n.ok?n.json():n.json().then((function(e){throw new Error(e.error||"Error")}))}))}function c(e){if(!e)return"";var t=document.createElement("div");return t.textContent=e,t.innerHTML}function s(e,t){var n=document.getElementById("toast-container"),r=document.createElement("div");r.className="toast "+(t||"success"),r.textContent=e,n.appendChild(r),setTimeout((function(){r.remove()}),3500)}function d(e){return e?new Date(e).toLocaleDateString("es-MX",{year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}):"—"}document.querySelectorAll(".bodega-tab").forEach((function(e){e.addEventListener("click",(function(){document.querySelectorAll(".bodega-tab").forEach((function(e){e.classList.remove("active")})),document.querySelectorAll(".bodega-section").forEach((function(e){e.classList.remove("active")})),e.classList.add("active");var t=document.getElementById("section-"+e.getAttribute("data-tab"));t&&t.classList.add("active");var n=e.getAttribute("data-tab");"subir"===n&&p(),"inventario"===n&&g()}))})),document.getElementById("btn-save-mapping").addEventListener("click",(function(){var e=document.getElementById("map-part-number").value.trim(),t=document.getElementById("map-price").value.trim(),n=document.getElementById("map-stock").value.trim(),r=document.getElementById("map-location").value.trim(),a=document.getElementById("mapping-status");if(!e||!t||!n)return a.textContent="Completa los campos obligatorios.",void(a.className="status-msg error");var c={part_number:e,price:t,stock:n,location:r||null};a.textContent="Guardando...",a.className="status-msg",i("/api/inventory/mapping",{method:"PUT",headers:o({"Content-Type":"application/json"}),body:JSON.stringify(c)}).then((function(){a.textContent="Mapeo guardado correctamente.",a.className="status-msg success",s("Mapeo guardado","success")})).catch((function(e){a.textContent=e.message||"Error al guardar.",a.className="status-msg error"}))}));var l=document.getElementById("drop-zone"),u=document.getElementById("file-input");function m(t){var n=(t.name||"").split(".").pop().toLowerCase();-1!==["text/csv","application/vnd.ms-excel","application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"].indexOf(t.type)||-1!==["csv","xls","xlsx"].indexOf(n)?t.size>10485760?s("El archivo excede 10MB.","error"):(e=t,document.getElementById("selected-file-name").textContent=t.name+" ("+(t.size/1024).toFixed(1)+" KB)",document.getElementById("selected-file").style.display="flex",document.getElementById("btn-upload").disabled=!1):s("Formato no soportado. Usa CSV o Excel.","error")}function p(){i("/api/inventory/uploads").then((function(e){var t=e.uploads||e.data||e||[],n=document.getElementById("history-body");Array.isArray(t)&&0!==t.length?n.innerHTML=t.map((function(e){return""+c(e.filename||e.archivo||"—")+""+(t=e.status||e.estado,''+c(n.label)+"")+(null!=e.imported_count?e.imported_count:null!=e.importados?e.importados:"—")+""+(null!=e.error_count?e.error_count:null!=e.errores?e.errores:"—")+""+d(e.created_at||e.fecha)+"";var t,n})).join(""):n.innerHTML='Sin cargas previas'})).catch((function(){document.getElementById("history-body").innerHTML='Error al cargar historial'}))}function g(){var e="?page="+t;n&&(e+="&q="+encodeURIComponent(n)),i("/api/inventory/items"+e).then((function(e){var t=e.data||e.items||[],n=e.pagination||{},r=document.getElementById("inv-body");if(!Array.isArray(t)||0===t.length)return r.innerHTML='Sin articulos en inventario',void f(n);r.innerHTML=t.map((function(e){return""+c(e.part_number)+""+c(e.name||e.nombre||"—")+""+(t=e.price||e.precio,n=parseFloat(t),(isNaN(n)?"—":"$"+n.toLocaleString("es-MX",{minimumFractionDigits:2,maximumFractionDigits:2}))+"")+(null!=e.stock?e.stock:null!=e.existencias?e.existencias:"—")+""+c(e.location||e.ubicacion||"—")+""+d(e.updated_at||e.actualizado)+"";var t,n})).join(""),f(n)})).catch((function(){document.getElementById("inv-body").innerHTML='Error al cargar inventario'}))}function f(e){var n=document.getElementById("inv-pagination");if(!e||!e.total_pages||e.total_pages<=1)n.innerHTML="";else{var r=e.page||e.current_page||1,o=e.total_pages,a="";a+="';var i=Math.max(1,r-2),c=Math.min(o,r+2);i>1&&(a+='',i>2&&(a+=""));for(var s=i;s<=c;s++)a+='";c..."),a+='"),a+="',n.innerHTML=a,n.querySelectorAll("button[data-page]").forEach((function(e){e.addEventListener("click",(function(){t=parseInt(e.getAttribute("data-page"),10),g()}))}))}}l.addEventListener("click",(function(){u.click()})),l.addEventListener("dragover",(function(e){e.preventDefault(),l.classList.add("dragover")})),l.addEventListener("dragleave",(function(){l.classList.remove("dragover")})),l.addEventListener("drop",(function(e){e.preventDefault(),l.classList.remove("dragover"),e.dataTransfer.files.length&&m(e.dataTransfer.files[0])})),u.addEventListener("change",(function(){u.files.length&&m(u.files[0])})),document.getElementById("btn-clear-file").addEventListener("click",(function(){e=null,u.value="",document.getElementById("selected-file").style.display="none",document.getElementById("btn-upload").disabled=!0})),document.getElementById("btn-upload").addEventListener("click",(function(){if(e){var t=document.getElementById("btn-upload"),n=document.getElementById("upload-status");t.disabled=!0,n.textContent="Subiendo...",n.className="status-msg";var o=new FormData;o.append("file",e),fetch("/api/inventory/upload",{method:"POST",headers:{Authorization:"Bearer "+r()},body:o}).then((function(e){return 401===e.status?a().then((function(){return fetch("/api/inventory/upload",{method:"POST",headers:{Authorization:"Bearer "+r()},body:o})})):e})).then((function(e){return e.json().then((function(t){if(!e.ok)throw new Error(t.error||"Error al subir");return t}))})).then((function(r){n.textContent="",function(e){var t=document.getElementById("upload-result"),n=e.imported||e.imported_count||0,r=e.errors||e.error_count||0,o=e.error_samples||[],a="

Resultado de la Carga

";a+='
',a+='Importados: '+n+"",a+='Errores: '+r+"",a+="
",o.length&&(a+='
',a+="Ejemplos de errores:",o.forEach((function(e){a+="

"+c("string"==typeof e?e:JSON.stringify(e))+"

"})),a+="
");t.innerHTML=a,t.style.display="block"}(r),s("Archivo procesado correctamente.","success"),p(),e=null,u.value="",document.getElementById("selected-file").style.display="none",t.disabled=!0})).catch((function(e){n.textContent=e.message||"Error al subir archivo.",n.className="status-msg error",t.disabled=!1}))}})),document.getElementById("btn-inv-search").addEventListener("click",(function(){n=document.getElementById("inv-search").value.trim(),t=1,g()})),document.getElementById("inv-search").addEventListener("keydown",(function(e){"Enter"===e.key&&(n=this.value.trim(),t=1,g())})),document.getElementById("btn-clear-all").addEventListener("click",(function(){var e,t,n;e="Limpiar Inventario",t="Se eliminaran todos los articulos de tu inventario. Esta accion no se puede deshacer.",n=function(){i("/api/inventory/items",{method:"DELETE",headers:o()}).then((function(){s("Inventario limpiado correctamente.","success"),g()})).catch((function(e){s(e.message||"Error al limpiar inventario.","error")}))},document.getElementById("confirm-title").textContent=e,document.getElementById("confirm-msg").textContent=t,document.getElementById("confirm-modal").classList.add("active"),y=n}));var v,h,y=null;document.getElementById("confirm-cancel").addEventListener("click",(function(){document.getElementById("confirm-modal").classList.remove("active"),y=null})),document.getElementById("confirm-ok").addEventListener("click",(function(){document.getElementById("confirm-modal").classList.remove("active"),y&&(y(),y=null)})),document.getElementById("confirm-modal").addEventListener("click",(function(e){e.target===this&&(this.classList.remove("active"),y=null)})),v=r(),h=function(){var e=r();if(!e)return null;try{return JSON.parse(atob(e.split(".")[1])).role||null}catch(e){return null}}(),(v&&("BODEGA"===h||"ADMIN"===h)||(window.location.href="/login.html",0))&&i("/api/inventory/mapping").then((function(e){e.part_number&&(document.getElementById("map-part-number").value=e.part_number),e.price&&(document.getElementById("map-price").value=e.price),e.stock&&(document.getElementById("map-stock").value=e.stock),e.location&&(document.getElementById("map-location").value=e.location)})).catch((function(){}))}(); \ No newline at end of file diff --git a/dashboard/captura.min.css b/dashboard/captura.min.css new file mode 100644 index 0000000..b8bfbe7 --- /dev/null +++ b/dashboard/captura.min.css @@ -0,0 +1,660 @@ +/* ============================================================ + captura.css -- Styles for Nexus Autoparts Data Entry + ============================================================ */ + +/* --- Tabs --- */ +.captura-tabs { + display: flex; + gap: 0; + border-bottom: 2px solid var(--border); + margin-bottom: 1.5rem; +} + +.captura-tab { + padding: 0.8rem 1.8rem; + background: transparent; + border: none; + color: var(--text-secondary); + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + border-bottom: 3px solid transparent; + transition: all 0.2s; + position: relative; + bottom: -2px; +} + +.captura-tab:hover { + color: var(--text-primary); + background: var(--bg-hover); +} + +.captura-tab.active { + color: var(--accent); + border-bottom-color: var(--accent); +} + +.captura-tab .tab-badge { + background: var(--accent); + color: #fff; + font-size: 0.7rem; + padding: 0.15rem 0.5rem; + border-radius: 10px; + margin-left: 0.5rem; + font-weight: 700; +} + +.captura-section { + display: none; +} + +.captura-section.active { + display: block; +} + +/* --- Vehicle Selector (Section 1) --- */ +.vehicle-filters { + display: flex; + gap: 1rem; + margin-bottom: 1rem; + flex-wrap: wrap; + align-items: flex-end; +} + +.vehicle-filters .filter-group { + display: flex; + flex-direction: column; + gap: 0.3rem; +} + +.vehicle-filters label { + font-size: 0.75rem; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.vehicle-filters select, +.vehicle-filters input { + padding: 0.5rem 0.8rem; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text-primary); + font-size: 0.9rem; + min-width: 160px; +} + +.vehicle-filters select:focus, +.vehicle-filters input:focus { + outline: none; + border-color: var(--accent); +} + +/* --- Vehicle List --- */ +.vehicle-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 0.8rem; + max-height: 400px; + overflow-y: auto; + padding-right: 0.5rem; +} + +.vehicle-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + padding: 1rem; + cursor: pointer; + transition: all 0.2s; +} + +.vehicle-card:hover { + border-color: var(--accent); + background: var(--bg-hover); +} + +.vehicle-card .vc-brand { + font-weight: 700; + font-size: 0.95rem; + color: var(--accent); +} + +.vehicle-card .vc-model { + font-size: 1.1rem; + font-weight: 600; + margin: 0.2rem 0; +} + +.vehicle-card .vc-details { + font-size: 0.8rem; + color: var(--text-secondary); +} + +.vehicle-card .vc-parts-count { + margin-top: 0.5rem; + font-size: 0.75rem; + color: var(--success); +} + +/* --- Vehicle Header (when editing) --- */ +.vehicle-header { + background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-hover) 100%); + border: 1px solid var(--accent); + border-radius: 12px; + padding: 1rem 1.5rem; + margin-bottom: 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; +} + +.vehicle-header .vh-info { + display: flex; + gap: 1.5rem; + align-items: center; + flex-wrap: wrap; +} + +.vehicle-header .vh-label { + font-size: 0.7rem; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.vehicle-header .vh-value { + font-size: 1.1rem; + font-weight: 700; +} + +.vehicle-header .vh-brand { color: var(--accent); } + +.vehicle-header .vh-actions { + display: flex; + gap: 0.5rem; +} + +/* --- Part Groups Table --- */ +.category-section { + margin-bottom: 1.5rem; +} + +.category-header { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 8px 8px 0 0; + padding: 0.6rem 1rem; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + user-select: none; +} + +.category-header:hover { + background: var(--bg-hover); +} + +.category-header h3 { + font-size: 0.9rem; + font-weight: 700; + color: var(--accent); +} + +.category-header .cat-toggle { + font-size: 0.8rem; + color: var(--text-secondary); + transition: transform 0.2s; +} + +.category-header.collapsed .cat-toggle { + transform: rotate(-90deg); +} + +.category-body { + border: 1px solid var(--border); + border-top: none; + border-radius: 0 0 8px 8px; +} + +.category-body.collapsed { + display: none; +} + +.group-section { + border-bottom: 1px solid var(--border); + padding: 0.8rem 1rem; +} + +.group-section:last-child { + border-bottom: none; +} + +.group-name { + font-size: 0.85rem; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 0.5rem; +} + +/* --- Part Rows --- */ +.part-rows { + display: flex; + flex-direction: column; + gap: 0.4rem; + margin-bottom: 0.4rem; +} + +.part-row { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.part-row input { + padding: 0.4rem 0.6rem; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-primary); + font-size: 0.85rem; +} + +.part-row input:focus { + outline: none; + border-color: var(--accent); +} + +.part-row .pr-oem { + width: 160px; + font-family: monospace; +} + +.part-row .pr-name { + flex: 1; + min-width: 150px; +} + +.part-row .pr-qty { + width: 50px; + text-align: center; +} + +.part-row .pr-btn { + padding: 0.3rem 0.6rem; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 0.8rem; + font-weight: 600; + transition: all 0.2s; +} + +.part-row .pr-save { + background: var(--success); + color: #fff; +} + +.part-row .pr-save:hover { background: #1ea34e; } + +.part-row .pr-delete { + background: var(--danger); + color: #fff; +} + +.part-row .pr-delete:hover { background: #cc3333; } + +.part-row.saved { + background: rgba(34, 197, 94, 0.08); + border-radius: 6px; + padding: 0.2rem 0.4rem; +} + +.part-row.saved input { + background: transparent; + border-color: var(--success); + color: var(--success); +} + +.btn-add-part { + background: transparent; + border: 1px dashed var(--border); + border-radius: 6px; + padding: 0.3rem 0.8rem; + color: var(--text-secondary); + font-size: 0.8rem; + cursor: pointer; + transition: all 0.2s; +} + +.btn-add-part:hover { + border-color: var(--accent); + color: var(--accent); +} + +/* --- Progress Bar --- */ +.progress-bar { + background: var(--bg-secondary); + border-radius: 10px; + height: 8px; + overflow: hidden; + margin: 0.5rem 0; +} + +.progress-bar .progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--accent), var(--success)); + border-radius: 10px; + transition: width 0.3s; +} + +.progress-text { + font-size: 0.75rem; + color: var(--text-secondary); +} + +/* --- Section 2: Intercambios --- */ +.part-detail-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + padding: 1rem; + margin-bottom: 1rem; +} + +.part-detail-card .pdc-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.8rem; +} + +.part-detail-card .pdc-oem { + font-family: monospace; + font-size: 1rem; + font-weight: 700; + color: var(--accent); +} + +.part-detail-card .pdc-name { + font-size: 0.85rem; + color: var(--text-secondary); +} + +.part-detail-card .pdc-group { + font-size: 0.75rem; + color: var(--text-secondary); + background: var(--bg-hover); + padding: 0.2rem 0.5rem; + border-radius: 4px; +} + +.aftermarket-table { + width: 100%; + border-collapse: collapse; + font-size: 0.85rem; +} + +.aftermarket-table th { + text-align: left; + padding: 0.5rem; + border-bottom: 1px solid var(--border); + color: var(--text-secondary); + font-size: 0.75rem; + text-transform: uppercase; +} + +.aftermarket-table td { + padding: 0.4rem 0.5rem; + border-bottom: 1px solid rgba(42, 42, 58, 0.5); +} + +.aftermarket-form { + display: flex; + gap: 0.5rem; + align-items: flex-end; + flex-wrap: wrap; + margin-top: 0.8rem; + padding-top: 0.8rem; + border-top: 1px dashed var(--border); +} + +.aftermarket-form .af-field { + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.aftermarket-form label { + font-size: 0.7rem; + color: var(--text-secondary); + text-transform: uppercase; +} + +.aftermarket-form select, +.aftermarket-form input { + padding: 0.4rem 0.6rem; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-primary); + font-size: 0.85rem; +} + +.aftermarket-form select:focus, +.aftermarket-form input:focus { + outline: none; + border-color: var(--accent); +} + +/* --- Section 3: Imágenes --- */ +.image-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + padding: 1rem; + display: flex; + gap: 1rem; + align-items: center; + margin-bottom: 0.8rem; +} + +.image-card .ic-preview { + width: 100px; + height: 100px; + background: var(--bg-secondary); + border: 2px dashed var(--border); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-secondary); + font-size: 0.75rem; + overflow: hidden; + flex-shrink: 0; +} + +.image-card .ic-preview img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.image-card .ic-info { + flex: 1; +} + +.image-card .ic-oem { + font-family: monospace; + font-weight: 700; + color: var(--accent); +} + +.image-card .ic-name { + font-size: 0.85rem; + color: var(--text-secondary); + margin-bottom: 0.5rem; +} + +.image-card .ic-upload { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.image-card .ic-upload input[type="file"] { + font-size: 0.8rem; + color: var(--text-secondary); +} + +/* --- Search bar --- */ +.captura-search { + display: flex; + gap: 0.8rem; + margin-bottom: 1rem; + align-items: center; +} + +.captura-search input { + padding: 0.5rem 0.8rem; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text-primary); + font-size: 0.9rem; + flex: 1; + max-width: 400px; +} + +.captura-search input:focus { + outline: none; + border-color: var(--accent); +} + +/* --- Pagination --- */ +.captura-pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 0.5rem; + margin-top: 1rem; + padding: 0.5rem; +} + +.captura-pagination button { + padding: 0.4rem 0.8rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-primary); + cursor: pointer; + font-size: 0.85rem; +} + +.captura-pagination button:hover { + border-color: var(--accent); +} + +.captura-pagination button:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.captura-pagination .page-info { + font-size: 0.8rem; + color: var(--text-secondary); +} + +/* --- Empty state --- */ +.empty-state { + text-align: center; + padding: 3rem; + color: var(--text-secondary); +} + +.empty-state .es-icon { + font-size: 2.5rem; + margin-bottom: 0.5rem; +} + +.empty-state .es-text { + font-size: 0.9rem; +} + +/* --- Toast notifications --- */ +.toast { + position: fixed; + bottom: 2rem; + right: 2rem; + padding: 0.8rem 1.5rem; + border-radius: 10px; + color: #fff; + font-weight: 600; + font-size: 0.9rem; + z-index: 9999; + animation: toastIn 0.3s ease; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); +} + +.toast.success { background: var(--success); } +.toast.error { background: var(--danger); } + +@keyframes toastIn { + from { transform: translateY(20px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +/* --- Loading spinner --- */ +.loading { + display: flex; + justify-content: center; + padding: 2rem; +} + +.spinner { + width: 30px; + height: 30px; + border: 3px solid var(--border); + border-top-color: var(--accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* --- Layout --- */ +.captura-container { + max-width: 1200px; + margin: 0 auto; + padding: 5rem 2rem 2rem; +} + +/* --- Status tabs for vehicles --- */ +.status-tabs { + display: flex; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.status-tab { + padding: 0.4rem 1rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 20px; + color: var(--text-secondary); + font-size: 0.8rem; + cursor: pointer; + transition: all 0.2s; +} + +.status-tab:hover { border-color: var(--accent); } + +.status-tab.active { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} diff --git a/dashboard/captura.min.js b/dashboard/captura.min.js new file mode 100644 index 0000000..042d0f3 --- /dev/null +++ b/dashboard/captura.min.js @@ -0,0 +1 @@ +!function(){"use strict";var e=null,t=null,a=[],n=[],r="pending",i=1;function o(e,t){var a=document.createElement("div");a.className="toast "+(t||"success"),a.textContent=e,document.body.appendChild(a),setTimeout((function(){a.remove()}),3e3)}function c(e,t){return t=t||{},fetch(""+e,t).then((function(e){return e.ok?e.json():e.json().then((function(e){throw new Error(e.error||"Error")}))}))}function d(e){if(!e)return"";var t=document.createElement("div");return t.textContent=e,t.innerHTML}document.querySelectorAll(".captura-tab").forEach((function(e){e.addEventListener("click",(function(){document.querySelectorAll(".captura-tab").forEach((function(e){e.classList.remove("active")})),document.querySelectorAll(".captura-section").forEach((function(e){e.classList.remove("active")})),e.classList.add("active");var t=e.getAttribute("data-tab");document.getElementById("section-"+t).classList.add("active"),"aftermarket"===t&&g(),"images"===t&&h()}))})),document.querySelectorAll(".status-tab").forEach((function(e){e.addEventListener("click",(function(){document.querySelectorAll(".status-tab").forEach((function(e){e.classList.remove("active")})),e.classList.add("active"),r=e.getAttribute("data-status"),i=1,s()}))})),document.getElementById("oem-brand-filter").addEventListener("change",(function(){i=1,s()}));var l=null;function s(){var n=document.getElementById("oem-brand-filter").value,l=document.getElementById("oem-model-filter").value,g=document.getElementById("oem-vehicle-list");g.innerHTML='
';var y="pending"===r?"/api/captura/vehicles/pending":"/api/captura/vehicles/in-progress",h="?page="+i+"&per_page=30";n&&(h+="&brand="+encodeURIComponent(n)),l&&(h+="&model="+encodeURIComponent(l)),c(y+h).then((function(n){var l=n.data||[];if(0===l.length)return g.innerHTML='
📋
No hay vehiculos '+("pending"===r?"pendientes":"en progreso")+"
",void(document.getElementById("oem-vehicle-pagination").innerHTML="");g.innerHTML=l.map((function(e){return'
'+d(e.brand)+'
'+d(e.model)+'
'+e.year+" · "+d(e.engine)+(e.trim_level?" · "+d(e.trim_level):"")+"
"+(e.parts_count?'
'+e.parts_count+" partes registradas
":"")+"
"})).join(""),g.querySelectorAll(".vehicle-card").forEach((function(n){n.addEventListener("click",(function(){var r;r=parseInt(n.getAttribute("data-mye")),e=r,document.getElementById("oem-vehicle-select").style.display="none",document.getElementById("oem-part-entry").style.display="block",c("/api/captura/vehicles/"+r+"/status",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({status:"in_progress"})}),function(n){c("/api/captura/vehicles/"+n+"/parts").then((function(n){t=n.vehicle,a=n.parts||[],document.getElementById("oem-vehicle-header").innerHTML='
Marca
'+d(t.brand)+'
Modelo
'+d(t.model)+'
Ano
'+t.year+'
Motor
'+d(t.engine)+"
"+(t.trim_level?'
Trim
'+d(t.trim_level)+"
":"")+'
',document.getElementById("btn-back-vehicles").addEventListener("click",p),document.getElementById("btn-complete-vehicle").addEventListener("click",m),function(t,n){var r=document.getElementById("oem-groups-container"),i={};t.forEach((function(e){i[e.category]||(i[e.category]={id:e.id_part_category,groups:[]}),i[e.category].groups.push(e)}));var l="";Object.keys(i).forEach((function(e){var t=i[e],a=n.filter((function(e){return t.groups.some((function(t){return t.id_part_group===e.group_id}))}));l+='

'+d(e)+" ("+a.length+')

',t.groups.forEach((function(e){var t=n.filter((function(t){return t.group_id===e.id_part_group}));l+='
'+d(e.group_name)+'
',t.forEach((function(e){l+=v(e)})),l+='
'})),l+="
"})),r.innerHTML=l,r.querySelectorAll(".category-header").forEach((function(e){e.addEventListener("click",(function(){var t=e.getAttribute("data-cat"),a=r.querySelector('[data-cat-body="'+t+'"]');e.classList.toggle("collapsed"),a.classList.toggle("collapsed")}))})),r.querySelectorAll(".btn-add-part").forEach((function(t){t.addEventListener("click",(function(){var n,r,i;n=parseInt(t.getAttribute("data-group-id")),r=document.querySelector('[data-group-parts="'+n+'"]'),(i=document.createElement("div")).className="part-row",i.innerHTML='',r.appendChild(i),i.querySelector(".pr-oem").focus(),i.querySelector(".pr-oem").addEventListener("blur",(function(){var e=this.value.trim();e&&c("/api/captura/parts/check-oem?oem="+encodeURIComponent(e)).then((function(e){e.exists&&(i.querySelector(".pr-name").value=e.part.name_part||"",i.querySelector(".pr-name").style.borderColor="var(--success)",i.dataset.existingPartId=e.part.id_part)}))})),i.querySelector(".pr-save").addEventListener("click",(function(){!function(t,n){var r=t.querySelector(".pr-oem").value.trim(),i=t.querySelector(".pr-name").value.trim(),d=parseInt(t.querySelector(".pr-qty").value)||1;if(!r)return o("Ingresa el numero OEM","error"),void t.querySelector(".pr-oem").focus();var l=t.querySelector(".pr-save");l.disabled=!0,l.textContent="...";var s=t.dataset.existingPartId;function u(s){c("/api/admin/fitment",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model_year_engine_id:e,part_id:s,quantity_required:d})}).then((function(e){var l={id_vehicle_part:e.id,part_id:s,oem_part_number:r,name_part:i,quantity_required:d,group_id:n};a.push(l),t.outerHTML=v(l),f(),o("Parte guardada: "+r),document.querySelectorAll(".part-row.saved .pr-delete").forEach((function(e){e.onclick=function(){var t=e.closest(".part-row"),n=t.getAttribute("data-fitment-id");n?c("/api/admin/fitment/"+n,{method:"DELETE"}).then((function(){a=a.filter((function(e){return e.id_vehicle_part!==parseInt(n)})),t.remove(),f(),o("Parte eliminada")})).catch((function(e){o(e.message,"error")})):t.remove()}}))})).catch((function(e){o(e.message,"error"),l.disabled=!1,l.textContent="✓"}))}s?u(parseInt(s)):c("/api/admin/parts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oem_part_number:r,name:i||r,group_id:n})}).then((function(e){u(e.id)})).catch((function(e){o(e.message,"error"),l.disabled=!1,l.textContent="✓"}))}(i,n)})),i.querySelector(".pr-delete").addEventListener("click",(function(){i.remove()}))}))}))}(n.groups,a),f()}))}(r)}))})),u("oem-vehicle-pagination",n.pagination,(function(e){i=e,s()}))}))}function u(e,t,a){var n=document.getElementById(e);!t||t.total_pages<=1?n.innerHTML="":(n.innerHTML="Pag '+t.page+" de "+t.total_pages+" ("+t.total+" total)',n.querySelectorAll("button").forEach((function(e){e.addEventListener("click",(function(){a(parseInt(e.getAttribute("data-p")))}))})))}function p(){document.getElementById("oem-vehicle-select").style.display="block",document.getElementById("oem-part-entry").style.display="none",e=null,s()}function m(){0!==a.length?c("/api/captura/vehicles/"+e+"/status",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({status:"completed"})}).then((function(){o("Vehiculo completado"),p()})):o("Registra al menos una parte antes de marcar como terminado","error")}function v(e){return'
'}function f(){var e=a.length,t=Math.min(100,Math.round(e/63*100));document.getElementById("oem-progress-fill").style.width=t+"%",document.getElementById("oem-progress-text").textContent=e+" partes registradas",document.querySelectorAll(".category-header h3").forEach((function(e){var t=e.closest(".category-section").querySelectorAll(".part-row.saved"),a=e.textContent.replace(/\s*\(\d+\)$/,"");e.textContent=a+" ("+t.length+")"}))}document.getElementById("oem-model-filter").addEventListener("input",(function(){clearTimeout(l),l=setTimeout((function(){i=1,s()}),400)}));function g(e){e=e||1;var t=document.getElementById("aftermarket-search").value,a=document.getElementById("aftermarket-list");a.innerHTML='
';var r="?page="+e+"&per_page=20";t&&(r+="&search="+encodeURIComponent(t)),c("/api/captura/parts/without-aftermarket"+r).then((function(e){var t=e.data||[];if(0===t.length)return a.innerHTML='
No hay piezas sin intercambios
',void(document.getElementById("aftermarket-pagination").innerHTML="");a.innerHTML=t.map((function(e){return'
'+d(e.oem_part_number)+' '+d(e.name_part)+'
'+d(e.category)+" › "+d(e.group_name)+'
'})).join(""),t.forEach((function(e){y(e.id_part)})),a.querySelectorAll(".af-save-btn").forEach((function(e){e.addEventListener("click",(function(){!function(e){var t=e.getAttribute("data-part-id"),a=e.querySelector(".af-manufacturer").value,n=e.querySelector(".af-partnum").value.trim(),r=e.querySelector(".af-name").value.trim(),i=e.querySelector(".af-quality").value,d=e.querySelector(".af-price").value,l=e.querySelector(".af-warranty").value;if(!n)return void o("Ingresa el numero de parte aftermarket","error");c("/api/admin/aftermarket",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({oem_part_id:parseInt(t),manufacturer_id:parseInt(a),part_number:n,name:r,quality_tier:i,price_usd:d?parseFloat(d):null,warranty_months:l?parseInt(l):null})}).then((function(){o("Intercambio guardado: "+n),e.querySelector(".af-partnum").value="",e.querySelector(".af-name").value="",e.querySelector(".af-price").value="",e.querySelector(".af-warranty").value="",y(parseInt(t))})).catch((function(e){o(e.message,"error")}))}(e.closest(".part-detail-card"))}))})),u("aftermarket-pagination",e.pagination,(function(e){g(e)}))}))}function y(e){c("/api/captura/parts/"+e+"/aftermarket").then((function(t){var a=document.querySelector('[data-af-list="'+e+'"]');if(0!==t.length){var n='';t.forEach((function(e){n+=""})),n+="
Fabricante# ParteNombreCalidadPrecioGarantia
"+d(e.manufacturer)+""+d(e.part_number)+""+d(e.name||"")+""+d(e.quality||"")+""+(e.price_usd?"$"+e.price_usd:"")+""+(e.warranty_months||"")+"
",a.innerHTML=n}else a.innerHTML='

Sin intercambios registrados

'}))}function h(e){e=e||1;var t=document.getElementById("image-search").value,a=document.getElementById("image-list");a.innerHTML='
';var n="?page="+e+"&per_page=20";t&&(n+="&search="+encodeURIComponent(t)),c("/api/captura/parts/without-image"+n).then((function(e){var t=e.data||[];if(0===t.length)return a.innerHTML='
📷
No hay piezas sin imagen
',void(document.getElementById("image-pagination").innerHTML="");a.innerHTML=t.map((function(e){return'
Sin imagen
'+d(e.oem_part_number)+'
'+d(e.name_part)+" · "+d(e.group_name)+'
'})).join(""),a.querySelectorAll(".ic-file-input").forEach((function(e){e.addEventListener("change",(function(){var t=e.closest(".image-card"),a=t.querySelector(".ic-upload-btn"),n=t.querySelector(".ic-preview");if(e.files&&e.files[0]){a.disabled=!1;var r=new FileReader;r.onload=function(e){n.innerHTML=''},r.readAsDataURL(e.files[0])}}))})),a.querySelectorAll(".ic-upload-btn").forEach((function(e){e.addEventListener("click",(function(){!function(e){var t=e.getAttribute("data-part-id"),a=e.querySelector(".ic-file-input"),n=e.querySelector(".ic-upload-btn");if(!a.files||!a.files[0])return;n.disabled=!0,n.textContent="Subiendo...";var r=new FormData;r.append("image",a.files[0]),fetch("/api/captura/parts/"+t+"/image",{method:"POST",body:r}).then((function(e){return e.json()})).then((function(t){if(t.error)throw new Error(t.error);o("Imagen subida correctamente"),e.style.opacity="0.3",setTimeout((function(){e.remove()}),500)})).catch((function(e){o(e.message,"error"),n.disabled=!1,n.textContent="Subir"}))}(e.closest(".image-card"))}))})),u("image-pagination",e.pagination,(function(e){h(e)}))}))}window.loadPartsWithoutAftermarket=g,window.loadPartsWithoutImage=h,c("/api/brands").then((function(e){var t=document.getElementById("oem-brand-filter");e.forEach((function(e){var a=document.createElement("option");a.value=e,a.textContent=e,t.appendChild(a)}))})),s(),c("/api/captura/manufacturers").then((function(e){n=e}))}(); \ No newline at end of file diff --git a/dashboard/catalog-public.min.js b/dashboard/catalog-public.min.js new file mode 100644 index 0000000..8d0e3fa --- /dev/null +++ b/dashboard/catalog-public.min.js @@ -0,0 +1 @@ +!function(){"use strict";var e={level:"brands",brand:null,model:null,year:null,engine:null,category:null,group:null,partType:null,nxGroup:null,nxSubgroup:null,nxPartType:null,region:"north-america",mode:"local"===localStorage.getItem("catalog_mode")?"local":"oem",page:1,totalPages:1};function n(){document.querySelectorAll("#modeToggle button").forEach((function(n){n.classList.toggle("is-active",n.getAttribute("data-mode")===e.mode)}))}window.setCatalogMode=function(a){if(("oem"===a||"local"===a)&&a!==e.mode){e.mode=a,localStorage.setItem("catalog_mode",a),n();var t=!(!e.engine||!e.engine.id_mye);if(e.category=e.group=e.partType=null,e.nxGroup=e.nxSubgroup=e.nxPartType=null,e.page=1,t)return e.level="categories",void v();e.brand=e.model=e.year=e.engine=null,e.level="brands",d()}},window.setRegion=function(n){e.region=n,document.querySelectorAll(".region-btn").forEach((function(e){e.classList.toggle("is-active",e.dataset.region===n)})),e.brand=e.model=e.year=e.engine=null,e.category=e.group=e.partType=null,e.nxGroup=e.nxSubgroup=e.nxPartType=null,d()};var a="/api/catalog",t=document.getElementById("content"),r=document.getElementById("breadcrumb"),o=document.getElementById("searchInput"),i=new URLSearchParams(window.location.search).get("brand");n(),i?fetch(a+"/brands?mode="+e.mode).then((function(e){return e.json()})).then((function(n){var a=n.find((function(e){return e.id_brand==i}));a?(e.brand={id:a.id_brand,name:a.name_brand},e.level="models",p()):d()})).catch((function(){d()})):d();var s=document.createElement("div");function l(){var n=[];if(n.push('Catalogo'),e.brand&&(n.push('/'),n.push(''+_(e.brand.name)+"")),e.model&&(n.push('/'),n.push(''+_(e.model.name)+"")),e.year&&(n.push('/'),n.push(''+_(String(e.year.value))+"")),e.engine){n.push('/');var a=e.engine.name+(e.engine.trim?" ("+e.engine.trim+")":"");n.push(''+_(a)+"")}e.nxGroup?(n.push('/'),n.push(''+_(e.nxGroup.name)+"")):e.category&&(n.push('/'),n.push(''+_(e.category.name)+"")),e.nxSubgroup?(n.push('/'),e.nxPartType?n.push(''+_(e.nxSubgroup.name)+""):n.push(""+_(e.nxSubgroup.name)+"")):e.group&&(n.push('/'),e.partType?n.push(''+_(e.group.name)+""):n.push(""+_(e.group.name)+"")),e.nxPartType?(n.push('/'),n.push(""+_(e.nxPartType.name)+"")):e.partType&&(n.push('/'),n.push(""+_(e.partType.name)+"")),"search"===e.level&&(n.push('/'),n.push("Busqueda")),r.innerHTML=n.join("")}function c(){e.category=e.group=e.partType=null,e.nxGroup=e.nxSubgroup=e.nxPartType=null}function d(){e.level="brands",l(),t.innerHTML='
Cargando marcas...
',fetch(a+"/brands?region="+(e.region||"north-america")+"&mode="+e.mode).then((function(e){return e.json()})).then((function(e){var n='

Selecciona una Marca

",t.innerHTML=n})).catch((function(){t.innerHTML='
Error cargando marcas.
'}))}function p(){l(),t.innerHTML='
Cargando modelos...
',fetch(a+"/models?brand_id="+e.brand.id).then((function(e){return e.json()})).then((function(n){var a="

"+_(e.brand.name)+' — Modelos

",t.innerHTML=a})).catch((function(){t.innerHTML='
Error cargando modelos.
'}))}function u(){l(),t.innerHTML='
Cargando anos...
',fetch(a+"/years?model_id="+e.model.id).then((function(e){return e.json()})).then((function(n){var a="

"+_(e.brand.name)+" "+_(e.model.name)+' — Anos

",t.innerHTML=a})).catch((function(){t.innerHTML='
Error cargando anos.
'}))}function g(){l(),t.innerHTML='
Cargando motores...
',fetch(a+"/engines?model_id="+e.model.id+"&year_id="+e.year.id).then((function(e){return e.json()})).then((function(n){var a="

"+_(e.brand.name)+" "+_(e.model.name)+" "+e.year.value+" — Motor

";a+='",t.innerHTML=a})).catch((function(){t.innerHTML='
Error cargando motores.
'}))}function v(){"local"===e.mode?(e.level="categories",l(),t.innerHTML='
Cargando categorias Local...
',fetch(a+"/categories?mode=local&mye_id="+e.engine.id_mye).then((function(e){return e.json()})).then((function(e){var n=e&&e.data||[];if(n.length){var a='

Categorias (Local · '+n.length+")

";a+='",t.innerHTML=a}else t.innerHTML='

Categorias (Local)

Ninguna parte de este vehiculo mapea al catalogo Local.
'})).catch((function(){t.innerHTML='
Error cargando categorias Local.
'}))):(l(),t.innerHTML='
Cargando categorias...
',fetch(a+"/categories?mye_id="+e.engine.id_mye).then((function(e){return e.json()})).then((function(e){if(e.length){var n='

Categorias

",t.innerHTML=n}else t.innerHTML='

Categorias

No se encontraron categorias con partes para este vehiculo.
'})).catch((function(){t.innerHTML='
Error cargando categorias.
'})))}function m(){e.level="groups",l(),t.innerHTML='
Cargando subcategorias...
';var n=a+"/groups?mode=local&mye_id="+e.engine.id_mye+"&category_slug="+encodeURIComponent(e.nxGroup.slug);fetch(n).then((function(e){return e.json()})).then((function(n){var a=n&&n.data||[];if(a.length){var r="

"+_(e.nxGroup.name)+' ('+a.length+" subcategorias)

";r+='",t.innerHTML=r}else t.innerHTML="

"+_(e.nxGroup.name)+'

Sin subcategorias.
'})).catch((function(){t.innerHTML='
Error cargando subcategorias.
'}))}function h(){e.level="part_types",l(),t.innerHTML='
Cargando tipos de parte...
';var n=a+"/part-types?mode=local&mye_id="+e.engine.id_mye+"&group_slug="+encodeURIComponent(e.nxGroup.slug)+"&subgroup_slug="+encodeURIComponent(e.nxSubgroup.slug);fetch(n).then((function(e){return e.json()})).then((function(n){var a=n&&n.data||[];if(a.length){if(1===a.length)return e.nxPartType={slug:a[0].slug,name:a[0].name},e.level="parts",e.page=1,void b();var r="

"+_(e.nxSubgroup.name)+' ('+a.length+" tipos)

";r+='",t.innerHTML=r}else t.innerHTML="

"+_(e.nxSubgroup.name)+'

Sin tipos de parte.
'})).catch((function(){t.innerHTML='
Error cargando tipos de parte.
'}))}function y(){l(),t.innerHTML='
Cargando grupos...
',fetch(a+"/groups?mye_id="+e.engine.id_mye+"&category_id="+e.category.id).then((function(e){return e.json()})).then((function(n){if(n.length){var a="

"+_(e.category.name)+'

",t.innerHTML=a}else t.innerHTML="

"+_(e.category.name)+'

No se encontraron sub-grupos.
'})).catch((function(){t.innerHTML='
Error cargando grupos.
'}))}function f(){e.level="part_types",l(),t.innerHTML='
Cargando tipos de parte...
',fetch(a+"/part-types?mye_id="+e.engine.id_mye+"&group_id="+e.group.id).then((function(e){return e.json()})).then((function(n){var a=n.data||[];if(!a.length)return e.level="parts",e.page=1,void b();if(1===a.length)return e.partType={slug:a[0].slug,name:a[0].name},e.level="parts",e.page=1,void b();var r="

"+_(e.group.name)+' ('+a.length+" tipos)

";r+='",t.innerHTML=r})).catch((function(){t.innerHTML='
Error cargando tipos de parte.
'}))}function b(){var n;if(l(),t.innerHTML='
Cargando partes...
',e.nxGroup&&e.nxSubgroup&&e.nxPartType)n=a+"/parts?mode=local&mye_id="+e.engine.id_mye+"&page="+e.page+"&nexpart_group="+encodeURIComponent(e.nxGroup.slug)+"&nexpart_subgroup="+encodeURIComponent(e.nxSubgroup.slug)+"&nexpart_part_type="+encodeURIComponent(e.nxPartType.slug);else{var r=e.partType?"&part_type="+encodeURIComponent(e.partType.slug):"";n=a+"/parts?mye_id="+e.engine.id_mye+"&group_id="+e.group.id+"&page="+e.page+"&mode="+e.mode+r}var o=e.nxPartType?e.nxPartType.name:e.nxSubgroup?e.nxSubgroup.name:e.partType?e.partType.name:e.group?e.group.name:"Partes";fetch(n).then((function(e){return e.json()})).then((function(n){var a=n.data,r=n.pagination;e.totalPages=r.total_pages;var i="local"===e.mode;if(a.length){var s="

"+_(o)+' ('+r.total+" partes)

";s+='
',a.forEach((function(e){var n="";if(i&&(1===e.priority_tier?n=" part-row--tier1":2===e.priority_tier&&(n=" part-row--tier2")),s+='
',s+="
",i&&e.manufacturer){var a=1===e.priority_tier?'':"";s+='
'+_(e.manufacturer)+""+a+"
"}i&&e.part_number?s+='
'+_(e.part_number)+' · OEM: '+_(e.oem_part_number)+"
":s+='
'+_(e.oem_part_number)+"
",s+='
'+_(e.name||"")+"
",e.description&&(s+='
'+_(e.description)+"
"),i&&(e.in_stock_network?s+='
En stock en '+e.bodega_count+" bodega"+(e.bodega_count>1?"s":"")+"
":s+='
Consultar disponibilidad
'),s+='',s+="
",e.image_url&&(s+=''),s+="
"})),s+="
",r.total_pages>1&&(s+='"),t.innerHTML=s}else t.innerHTML="

"+_(o)+'

No se encontraron partes.
'})).catch((function(){t.innerHTML='
Error cargando partes.
'}))}function _(e){if(!e)return"";var n=document.createElement("div");return n.textContent=e,n.innerHTML}function x(e){return _(e).replace(/'/g,"\\'").replace(/"/g,""")}s.style.cssText="display:none;padding:3px 10px;font-size:12px;color:var(--color-text-accent);background:var(--color-primary-muted);border:1px dashed var(--color-border-accent);border-radius:4px;margin-top:4px;",o.parentElement.after(s),o.addEventListener("input",(function(){var e=this.value.trim();if(e.length>=3){var n=function(e){if(!e)return"keyword";var n=e.trim(),a=n.replace(/[\s\-]/g,"").toUpperCase();if(/^[A-HJ-NPR-Z0-9]{17}$/.test(a))return"vin";if(/^[A-Z]{3}[-\s]?\d{3,4}$/.test(n.toUpperCase()))return"plate";if(/[a-z]/.test(n))return"keyword";var t=n.split(/\s+/);if(t.some((function(e){return/^(19|20)\d{2}$/.test(e)}))&&t.length>1)return"keyword";var r=n.toUpperCase();return/^[A-Z0-9]{2,}[\-\/][A-Z0-9]{2,}([\-\/][A-Z0-9]+)*$/.test(r)&&a.length>=6||t.length>=2&&t.every((function(e){return/^[A-Z0-9]{1,}$/.test(e)}))&&a.length>=6||/^[A-Z0-9]{8,}$/.test(a)&&/[A-Z]/.test(a)&&/\d/.test(a)?"part_number":"keyword"}(e),a={vin:"🚗 VIN detectado",plate:"🔖 Placa detectada",part_number:"🔩 Numero de parte",keyword:null};a[n]?(s.textContent=a[n],s.style.display=""):s.style.display="none"}else s.style.display="none"})),o.addEventListener("keydown",(function(e){"Enter"===e.key&&doSearch()})),window.toggleTheme=function(){var e=document.documentElement,n="industrial"===e.getAttribute("data-theme")?"modern":"industrial";e.setAttribute("data-theme",n),localStorage.setItem("nexus-theme",n)},window.doSearch=function(){var n=o.value.trim();!n||n.length<2||(e.level="search",l(),t.innerHTML='
Buscando...
',fetch(a+"/search?q="+encodeURIComponent(n)).then((function(e){return e.json()})).then((function(e){!function(e){if(l(),!e.length)return void(t.innerHTML='

Busqueda

No se encontraron resultados.
');var n="

Resultados ("+e.length+')

';e.forEach((function(e){n+='
',n+="
",n+='
'+_(e.oem_part_number)+"
",n+='
'+_(e.name||"")+"
",e.vehicle_info&&(n+='
'+_(e.vehicle_info)+"
"),n+='',n+="
",e.image_url&&(n+=''),n+="
"})),n+="
",t.innerHTML=n}(e)})).catch((function(){t.innerHTML='
Error en la busqueda.
'})))},window.openDetail=function(e){var n=document.getElementById("detailModal"),t=document.getElementById("detailBody");t.innerHTML='
Cargando detalle...
',n.classList.add("open"),fetch(a+"/part/"+e).then((function(e){return e.json()})).then((function(e){!function(e,n){if(!e||!e.part)return void(n.innerHTML='
Parte no encontrada.
');var a=e.part,t="";t+='
'+_(a.oem_part_number)+"
",t+='
'+_(a.name||"")+"
",a.category_name&&(t+='
'+_(a.category_name)+(a.group_name?" / "+_(a.group_name):"")+"
");a.description&&(t+='
'+_(a.description)+"
");a.image_url&&(t+='
',t+='',t+="
");e.alternatives&&e.alternatives.length&&(t+='
',t+="

Alternativas y Cross-References ("+e.alternatives.length+")

",t+='',e.alternatives.forEach((function(e){t+="",t+='",t+="",t+="",t+="",t+=""})),t+="
NumeroFabricanteNombreTipo
'+_(e.part_number||"")+""+_(e.manufacturer||"")+""+_(e.name||"-")+""+_("aftermarket"===e.type?"Aftermarket":"Cross-Ref")+"
");e.bodegas&&e.bodegas.length&&(t+='
',t+="

Disponibilidad en Bodegas ("+e.bodegas.length+")

",t+='',e.bodegas.forEach((function(e){t+="",t+="",t+="",t+="",t+=""})),t+="
BodegaStockUbicacion
"+_(e.business_name||"")+""+e.stock+""+_(e.location||"-")+"
");n.innerHTML=t}(e,t)})).catch((function(){t.innerHTML='
Error cargando detalle.
'}))},window.closeDetail=function(){document.getElementById("detailModal").classList.remove("open")},document.getElementById("detailModal").addEventListener("click",(function(e){e.target===this&&closeDetail()})),window.catalogNav=function(n){"brands"===n?(e.brand=e.model=e.year=e.engine=null,c(),e.level="brands",d()):"models"===n?(e.model=e.year=e.engine=null,c(),e.level="models",p()):"years"===n?(e.year=e.engine=null,c(),e.level="years",u()):"engines"===n?(e.engine=null,c(),e.level="engines",g()):"categories"===n?(c(),e.level="categories",v()):"groups"===n?(e.group=e.partType=null,e.level="groups",y()):"part_types"===n?(e.partType=null,e.level="part_types",f()):"nx_subgroups"===n?(e.nxSubgroup=e.nxPartType=null,e.level="groups",m()):"nx_part_types"===n&&(e.nxPartType=null,e.level="part_types",h())},window.selectBrand=function(n,a){e.brand={id:n,name:a},e.level="models",p()},window.selectModel=function(n,a){e.model={id:n,name:a},e.level="years",u()},window.selectYear=function(n,a){e.year={id:n,value:a},e.level="engines",g()},window.selectEngine=function(n,a,t){e.engine={id_mye:n,name:a,trim:t},e.level="categories",v()},window.selectNxGroup=function(n,a){e.nxGroup={slug:n,name:a},e.nxSubgroup=null,e.nxPartType=null,e.level="groups",m()},window.selectNxSubgroup=function(n,a){e.nxSubgroup={slug:n,name:a},e.nxPartType=null,e.level="part_types",h()},window.selectNxPartType=function(n,a){e.nxPartType={slug:n,name:a},e.level="parts",e.page=1,b()},window.selectCategory=function(n,a){e.category={id:n,name:a},e.level="groups",y()},window.selectGroup=function(n,a){e.group={id:n,name:a},e.partType=null,e.level="part_types",f()},window.selectPartType=function(n,a){e.partType={slug:n,name:a},e.level="parts",e.page=1,b()},window.partsPage=function(n){e.page=n,b(),window.scrollTo({top:0,behavior:"smooth"})}}(); \ No newline at end of file diff --git a/dashboard/chat-public.min.css b/dashboard/chat-public.min.css new file mode 100644 index 0000000..35381ac --- /dev/null +++ b/dashboard/chat-public.min.css @@ -0,0 +1,237 @@ +/* ========================================================================== + NEXUS — Public Catalog Chat Widget + Reuses design tokens from tokens.css + ========================================================================== */ + +.chat-fab { + position: fixed; + bottom: 24px; + right: 24px; + z-index: 8000; + width: 52px; + height: 52px; + border-radius: var(--radius-full, 50%); + border: none; + cursor: pointer; + background: var(--color-accent, #F5A623); + color: #fff; + font-size: 1.5rem; + display: flex; + align-items: center; + justify-content: center; + box-shadow: var(--shadow-lg, 0 4px 12px rgba(0,0,0,0.3)); + transition: transform 0.2s ease, background 0.2s ease; +} + +.chat-fab:hover { + transform: scale(1.08); + background: var(--color-primary-hover, #e5952f); +} + +.chat-panel { + position: fixed; + bottom: 90px; + right: 24px; + z-index: 8001; + width: 400px; + height: 520px; + max-height: calc(100vh - 100px); + display: flex; + flex-direction: column; + background: var(--color-bg-elevated, #1a1a1a); + border: 1px solid var(--color-border, #333); + border-radius: var(--radius-xl, 16px); + box-shadow: var(--shadow-xl, 0 8px 32px rgba(0,0,0,0.4)); + overflow: hidden; + transform: translateY(20px) scale(0.95); + opacity: 0; + pointer-events: none; + transition: transform 0.25s ease, opacity 0.25s ease; +} + +.chat-panel.open { + transform: translateY(0) scale(1); + opacity: 1; + pointer-events: all; +} + +.chat-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + background: var(--color-accent, #F5A623); + color: #fff; + flex-shrink: 0; +} + +.chat-header h3 { + font-family: var(--font-heading, sans-serif); + font-size: 0.95rem; + font-weight: 600; + margin: 0; +} + +.chat-header-close { + background: none; + border: none; + color: #fff; + font-size: 1.2rem; + cursor: pointer; + padding: 4px; + line-height: 1; + opacity: 0.8; +} + +.chat-header-close:hover { opacity: 1; } + +.chat-messages { + flex: 1; + overflow-y: auto; + padding: 12px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.chat-msg { + max-width: 85%; + padding: 8px 12px; + border-radius: 12px; + font-size: 0.875rem; + line-height: 1.45; + word-wrap: break-word; +} + +.chat-msg.user { + align-self: flex-end; + background: var(--color-accent, #F5A623); + color: #fff; + border-bottom-right-radius: 4px; +} + +.chat-msg.ai { + align-self: flex-start; + background: var(--color-surface-2, rgba(255,255,255,0.06)); + color: var(--color-text-primary, #fff); + border-bottom-left-radius: 4px; +} + +.chat-typing { + align-self: flex-start; + display: none; + gap: 4px; + padding: 8px 12px; + background: var(--color-surface-2, rgba(255,255,255,0.06)); + border-radius: 12px; + border-bottom-left-radius: 4px; +} + +.chat-typing.visible { display: flex; } + +.chat-typing span { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--color-text-muted, #888); + animation: chatBounce 1.2s infinite; +} +.chat-typing span:nth-child(2) { animation-delay: 0.2s; } +.chat-typing span:nth-child(3) { animation-delay: 0.4s; } + +@keyframes chatBounce { + 0%, 60%, 100% { transform: translateY(0); } + 30% { transform: translateY(-5px); } +} + +.chat-parts { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 8px; +} + +.chat-part-card { + background: var(--color-bg-elevated, #1a1a1a); + border: 1px solid var(--color-border, #333); + border-radius: 8px; + padding: 8px 12px; + cursor: pointer; + transition: border-color 0.15s ease, background 0.15s ease; +} + +.chat-part-card:hover { + border-color: var(--color-accent, #F5A623); + background: var(--color-bg-base, #111); +} + +.chat-part-card .part-number { + font-family: var(--font-mono, monospace); + font-size: 0.75rem; + color: var(--color-accent, #F5A623); + font-weight: 600; +} + +.chat-part-card .part-name { + font-size: 0.875rem; + color: var(--color-text-primary, #fff); + margin-top: 2px; +} + +.chat-input-area { + display: flex; + gap: 8px; + padding: 12px; + border-top: 1px solid var(--color-border, #333); + flex-shrink: 0; +} + +.chat-input { + flex: 1; + padding: 8px 12px; + border: 1px solid var(--color-border, #333); + border-radius: 8px; + background: var(--color-bg-base, #111); + color: var(--color-text-primary, #fff); + font-size: 0.875rem; + font-family: var(--font-body, sans-serif); + resize: none; + outline: none; + min-height: 38px; + max-height: 80px; +} + +.chat-input:focus { + border-color: var(--color-accent, #F5A623); +} + +.chat-input::placeholder { + color: var(--color-text-muted, #888); +} + +.chat-send-btn { + width: 38px; + height: 38px; + border-radius: 8px; + border: none; + background: var(--color-accent, #F5A623); + color: #fff; + font-size: 1.1rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: background 0.15s ease; +} + +.chat-send-btn:hover { background: var(--color-primary-hover, #e5952f); } +.chat-send-btn:disabled { opacity: 0.5; cursor: not-allowed; } + +@media (max-width: 480px) { + .chat-panel { + width: calc(100vw - 16px); + right: 8px; + height: 60vh; + } +} diff --git a/dashboard/chat-public.min.js b/dashboard/chat-public.min.js new file mode 100644 index 0000000..7fa9053 --- /dev/null +++ b/dashboard/chat-public.min.js @@ -0,0 +1 @@ +!function(){"use strict";var e=!1,t=!1,n=[];function a(){var e=document.createElement("button");e.className="chat-fab",e.id="chatFab",e.title="Asistente IA",e.innerHTML="💬",e.setAttribute("aria-label","Abrir asistente IA");var t=document.createElement("div");t.className="chat-panel",t.id="chatPanel",t.innerHTML='

Asistente — Buscar partes

Hola, soy el asistente de Nexus Autoparts. Dime que refaccion buscas y te ayudo a encontrarla en el catalogo.
',document.body.appendChild(e),document.body.appendChild(t),e.addEventListener("click",s),document.getElementById("chatClose").addEventListener("click",s),document.getElementById("chatSend").addEventListener("click",c),document.getElementById("chatInput").addEventListener("keydown",(function(e){"Enter"!==e.key||e.shiftKey||(e.preventDefault(),c())})),document.getElementById("chatInput").addEventListener("input",(function(){this.style.height="auto",this.style.height=Math.min(this.scrollHeight,80)+"px"}))}function s(){e=!e;var t=document.getElementById("chatPanel"),n=document.getElementById("chatFab");e?(t.classList.add("open"),n.style.display="none",document.getElementById("chatInput").focus()):(t.classList.remove("open"),n.style.display="flex")}function c(){if(!t){var e=document.getElementById("chatInput"),a=e.value.trim();a&&(e.value="",e.style.height="auto",i(a,"user"),n.push({role:"user",content:a}),n.length>20&&n.splice(0,2),t=!0,document.getElementById("chatSend").disabled=!0,d(!0),fetch("/api/chat",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({message:a,history:n.slice(-10)})}).then((function(e){return e.json()})).then((function(e){if(e.error)i("Error: "+e.error,"ai");else{var t,a,c,d,l=e.response||"Sin respuesta.";i(l,"ai"),n.push({role:"assistant",content:l}),e.search_results&&e.search_results.length>0&&(t=e.search_results,a=document.getElementById("chatMessages"),c=document.getElementById("chatTyping"),(d=document.createElement("div")).className="chat-parts",t.slice(0,8).forEach((function(e){var t=document.createElement("div");t.className="chat-part-card";var n=e.name_es||e.name_part||"",a=e.oem_part_number||e.part_number||"",c=e.brand||"";t.innerHTML='
'+o(a)+'
'+o(n)+(c?' ('+o(c)+")":"")+"
",t.style.cursor="pointer",t.addEventListener("click",(function(){var e=document.getElementById("searchInput");e&&a&&(e.value=a,"function"==typeof window.doSearch&&window.doSearch(),s())})),d.appendChild(t)})),a.insertBefore(d,c),r())}})).catch((function(e){i("Error de conexion: "+e.message,"ai")})).finally((function(){t=!1,document.getElementById("chatSend").disabled=!1,d(!1)})))}}function i(e,t){var n=document.getElementById("chatMessages"),a=document.getElementById("chatTyping"),s=document.createElement("div");s.className="chat-msg "+t,s.textContent=e,n.insertBefore(s,a),r()}function d(e){var t=document.getElementById("chatTyping");t&&t.classList.toggle("visible",e),e&&r()}function r(){var e=document.getElementById("chatMessages");e&&(e.scrollTop=e.scrollHeight)}function o(e){if(!e)return"";var t=document.createElement("div");return t.textContent=e,t.innerHTML}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",a):a()}(); \ No newline at end of file diff --git a/dashboard/cuentas.min.css b/dashboard/cuentas.min.css new file mode 100644 index 0000000..ff54c71 --- /dev/null +++ b/dashboard/cuentas.min.css @@ -0,0 +1,282 @@ +/* ============================================================ + cuentas.css -- Accounts receivable styles + ============================================================ */ + +.cuentas-container { + max-width: 1200px; + margin: 0 auto; + padding: 5rem 2rem 2rem; +} + +/* --- Customer list --- */ +.cuentas-search { + display: flex; + gap: 0.8rem; + margin-bottom: 1rem; +} + +.cuentas-search input { + flex: 1; + max-width: 400px; + padding: 0.5rem 0.8rem; + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text-primary); + font-size: 0.9rem; +} + +.cuentas-search input:focus { + outline: none; + border-color: var(--accent); +} + +.customer-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 0.8rem; + margin-bottom: 1.5rem; +} + +.customer-card-item { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + padding: 1rem; + cursor: pointer; + transition: all 0.2s; +} + +.customer-card-item:hover { + border-color: var(--accent); + background: var(--bg-hover); +} + +.cci-name { + font-weight: 700; + font-size: 1rem; + margin-bottom: 0.2rem; +} + +.cci-rfc { + font-family: monospace; + font-size: 0.8rem; + color: var(--text-secondary); +} + +.cci-balance-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 0.5rem; +} + +.cci-balance { + font-size: 1.1rem; + font-weight: 700; +} + +.cci-balance.positive { color: var(--danger); } +.cci-balance.zero { color: var(--success); } + +.cci-limit { + font-size: 0.75rem; + color: var(--text-secondary); +} + +/* --- Customer detail view --- */ +.detail-view { + display: none; +} + +.detail-header { + background: linear-gradient(135deg, var(--bg-card) 0%, var(--bg-hover) 100%); + border: 1px solid var(--accent); + border-radius: 12px; + padding: 1.2rem 1.5rem; + margin-bottom: 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 1rem; +} + +.dh-info { + display: flex; + gap: 2rem; + flex-wrap: wrap; +} + +.dh-field .dh-label { + font-size: 0.7rem; + color: var(--text-secondary); + text-transform: uppercase; +} + +.dh-field .dh-value { + font-size: 1rem; + font-weight: 600; +} + +.dh-field .dh-value.accent { color: var(--accent); } +.dh-field .dh-value.danger { color: var(--danger); } +.dh-field .dh-value.success { color: var(--success); } + +/* --- Two-column layout for invoices/payments --- */ +.detail-columns { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.5rem; +} + +@media (max-width: 768px) { + .detail-columns { grid-template-columns: 1fr; } +} + +.detail-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + overflow: hidden; +} + +.detail-card h3 { + padding: 0.8rem 1rem; + border-bottom: 1px solid var(--border); + font-size: 0.9rem; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.detail-table { + width: 100%; + border-collapse: collapse; + font-size: 0.85rem; +} + +.detail-table th { + text-align: left; + padding: 0.5rem 0.6rem; + border-bottom: 1px solid var(--border); + color: var(--text-secondary); + font-size: 0.75rem; + text-transform: uppercase; +} + +.detail-table td { + padding: 0.4rem 0.6rem; + border-bottom: 1px solid rgba(42, 42, 58, 0.5); +} + +.status-badge { + display: inline-block; + padding: 0.15rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 600; +} + +.status-badge.pending { background: rgba(245, 158, 11, 0.15); color: var(--warning); } +.status-badge.partial { background: rgba(59, 130, 246, 0.15); color: var(--info); } +.status-badge.paid { background: rgba(34, 197, 94, 0.15); color: var(--success); } +.status-badge.cancelled { background: rgba(255, 68, 68, 0.15); color: var(--danger); } + +/* --- Payment form --- */ +.payment-form { + padding: 1rem; + border-top: 1px solid var(--border); +} + +.payment-form h4 { + font-size: 0.85rem; + color: var(--accent); + margin-bottom: 0.8rem; +} + +.pf-row { + display: flex; + gap: 0.5rem; + margin-bottom: 0.5rem; + flex-wrap: wrap; +} + +.pf-field { + display: flex; + flex-direction: column; + gap: 0.2rem; +} + +.pf-field label { + font-size: 0.7rem; + color: var(--text-secondary); + text-transform: uppercase; +} + +.pf-field input, +.pf-field select { + padding: 0.4rem 0.6rem; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-primary); + font-size: 0.85rem; +} + +.pf-field input:focus, +.pf-field select:focus { + outline: none; + border-color: var(--accent); +} + +/* --- Toast --- */ +.toast { + position: fixed; + bottom: 2rem; + right: 2rem; + padding: 0.8rem 1.5rem; + border-radius: 10px; + color: #fff; + font-weight: 600; + font-size: 0.9rem; + z-index: 9999; + animation: toastIn 0.3s ease; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); +} +.toast.success { background: var(--success); } +.toast.error { background: var(--danger); } +@keyframes toastIn { + from { transform: translateY(20px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} + +/* --- Pagination --- */ +.cuentas-pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 0.5rem; + margin-top: 1rem; +} + +.cuentas-pagination button { + padding: 0.4rem 0.8rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-primary); + cursor: pointer; + font-size: 0.85rem; +} + +.cuentas-pagination button:hover { border-color: var(--accent); } +.cuentas-pagination button:disabled { opacity: 0.4; cursor: not-allowed; } +.cuentas-pagination .page-info { font-size: 0.8rem; color: var(--text-secondary); } + +.empty-state { + text-align: center; + padding: 2rem; + color: var(--text-secondary); + font-size: 0.9rem; +} diff --git a/dashboard/cuentas.min.js b/dashboard/cuentas.min.js new file mode 100644 index 0000000..95e2937 --- /dev/null +++ b/dashboard/cuentas.min.js @@ -0,0 +1 @@ +!function(){"use strict";var e=null,t=1;function n(e,t){var n=document.createElement("div");n.className="toast "+(t||"success"),n.textContent=e,document.body.appendChild(n),setTimeout((function(){n.remove()}),3e3)}function a(e,t){return t=t||{},fetch(""+e,t).then((function(e){return e.ok?e.json():e.json().then((function(e){throw new Error(e.error||"Error")}))}))}function o(e){if(!e)return"";var t=document.createElement("div");return t.textContent=e,t.innerHTML}function i(e){return"$"+(parseFloat(e)||0).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g,",")}function c(e){return e?new Date(e).toLocaleDateString("es-MX",{day:"2-digit",month:"short",year:"numeric"}):""}var d=null;function r(){var e=document.getElementById("customer-search").value,n=document.getElementById("customer-grid");n.innerHTML='
Cargando...
';var c="?page="+t+"&per_page=30";e&&(c+="&search="+encodeURIComponent(e)),a("/api/pos/customers"+c).then((function(e){var a=e.data||[];if(0===a.length)return n.innerHTML='
No se encontraron clientes
',void(document.getElementById("customer-pagination").innerHTML="");n.innerHTML=a.map((function(e){return'
'+o(e.name)+'
'+o(e.rfc||"Sin RFC")+'
'+i(e.balance)+'Limite: '+i(e.credit_limit)+"
"})).join(""),n.querySelectorAll(".customer-card-item").forEach((function(e){e.addEventListener("click",(function(){s(parseInt(e.getAttribute("data-id")))}))}));var c=e.pagination,d=document.getElementById("customer-pagination");c.total_pages<=1?d.innerHTML="":(d.innerHTML="Pag '+c.page+"/"+c.total_pages+"',d.querySelectorAll("button").forEach((function(e){e.addEventListener("click",(function(){t=parseInt(e.getAttribute("data-p")),r()}))})))})).catch((function(e){console.error("Error loading customers:",e),n.innerHTML='
Error al cargar clientes
'}))}function s(t){e=t,document.getElementById("list-view").style.display="none",document.getElementById("detail-view").style.display="block",a("/api/pos/customers/"+t+"/statement").then((function(e){var t=e.customer;document.getElementById("dh-name").textContent=t.name,document.getElementById("dh-rfc").textContent=t.rfc||"Sin RFC";var n=document.getElementById("dh-balance");n.textContent=i(t.balance),n.className="dh-value "+(t.balance>0?"danger":"success"),document.getElementById("dh-limit").textContent=i(t.credit_limit),document.getElementById("dh-terms").textContent=t.payment_terms+" dias";var a=document.getElementById("invoice-list");0===e.invoices.length?a.innerHTML='Sin facturas':a.innerHTML=e.invoices.map((function(e){return''+o(e.folio)+""+c(e.date_issued)+""+i(e.total)+""+i(e.amount_paid)+''+e.status+""})).join("");var d=document.getElementById("payment-list");0===e.payments.length?d.innerHTML='Sin pagos':d.innerHTML=e.payments.map((function(e){return""+c(e.date_payment)+''+i(e.amount)+""+o(e.payment_method)+""+o(e.reference||"")+""+o(e.invoice_folio||"General")+""})).join("");var r=document.getElementById("pay-invoice"),s=e.invoices.filter((function(e){return"paid"!==e.status&&"cancelled"!==e.status})).map((function(e){return'"})).join("");r.innerHTML=''+s}))}document.getElementById("customer-search").addEventListener("input",(function(){clearTimeout(d),d=setTimeout((function(){t=1,r()}),400)})),document.getElementById("btn-back-list").addEventListener("click",(function(){document.getElementById("detail-view").style.display="none",document.getElementById("list-view").style.display="block",e=null,r()})),document.getElementById("btn-pay").addEventListener("click",(function(){var t=parseFloat(document.getElementById("pay-amount").value);if(!t||t<=0)n("Ingresa un monto valido","error");else{var o=document.getElementById("pay-invoice").value;a("/api/pos/payments",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({customer_id:e,amount:t,payment_method:document.getElementById("pay-method").value,reference:document.getElementById("pay-reference").value.trim()||null,invoice_id:o?parseInt(o):null,notes:document.getElementById("pay-notes").value.trim()||null})}).then((function(){n("Pago de "+i(t)+" registrado"),document.getElementById("pay-amount").value="",document.getElementById("pay-reference").value="",document.getElementById("pay-notes").value="",s(e)})).catch((function(e){n(e.message,"error")}))}})),r()}(); \ No newline at end of file diff --git a/dashboard/dashboard.min.js b/dashboard/dashboard.min.js new file mode 100644 index 0000000..b803d79 --- /dev/null +++ b/dashboard/dashboard.min.js @@ -0,0 +1 @@ +class VehicleDashboard{constructor(){this.currentView="brands",this.selectedBrand=null,this.selectedModel=null,this.selectedYear=null,this.selectedVehicleId=null,this.selectedCategory=null,this.selectedGroupId=null,this.selectedGroup=null,this.allVehicles=[],this.filteredVehicles=[],this.allCategories=[],this.allParts=[],this.stats={brands:0,models:0,vehicles:0,parts:0},this.currentDiagramZoom=1,this.lastFocusedElement=null,this.init()}async init(){await this.loadStats(),await this.showBrands(),this.bindFilterEvents(),this.bindKeyboardShortcuts(),this.bindSearchEvents(),this.initDarkMode()}bindSearchEvents(){const e=document.getElementById("searchInput");e&&e.addEventListener("keypress",(e=>{"Enter"===e.key&&this.searchPartNumber()}))}async loadStats(){try{const[e,t,a]=await Promise.all([fetch("/api/catalog/stats"),fetch("/api/brands"),fetch("/api/categories")]);if(e.ok){const t=await e.json();this.stats.brands=t.brands,this.stats.models=t.models,this.stats.vehicles=t.vehicles,this.stats.parts=t.parts;const a=e=>e>1e3?Math.floor(e/1e3)+"K+":e,s=document.getElementById("totalBrands"),n=document.getElementById("totalModels"),i=document.getElementById("totalParts");s&&(s.textContent=a(this.stats.brands)),n&&(n.textContent=a(this.stats.models)),i&&(i.textContent=a(this.stats.parts))}t.ok&&await t.json(),a.ok&&(this.allCategories=await a.json())}catch(e){console.error("Error loading stats:",e)}}updateBreadcrumb(){const e=document.getElementById("breadcrumb");let t=[];if("brands"===this.currentView)t.push({label:' Marcas',active:!0});else if("models"===this.currentView)t.push({label:' Marcas',action:"dashboard.goToBrands()"}),t.push({label:this.selectedBrand,active:!0});else if("vehicles"===this.currentView)t.push({label:' Marcas',action:"dashboard.goToBrands()"}),t.push({label:this.selectedBrand,action:`dashboard.goToModels('${this.selectedBrand.replace(/'/g,"\\'")}')`}),t.push({label:this.selectedModel,active:!0});else if("categories"===this.currentView)t.push({label:' Marcas',action:"dashboard.goToBrands()"}),t.push({label:this.selectedBrand,action:`dashboard.goToModels('${this.selectedBrand.replace(/'/g,"\\'")}')`}),t.push({label:this.selectedModel,action:`dashboard.goToVehicles('${this.selectedBrand.replace(/'/g,"\\'")}', '${this.selectedModel.replace(/'/g,"\\'")}')`}),this.selectedYear&&t.push({label:this.selectedYear}),t.push({label:"Categorías",active:!0});else if("groups"===this.currentView)t.push({label:' Marcas',action:"dashboard.goToBrands()"}),t.push({label:this.selectedBrand,action:`dashboard.goToModels('${this.selectedBrand.replace(/'/g,"\\'")}')`}),t.push({label:this.selectedModel,action:`dashboard.goToVehicles('${this.selectedBrand.replace(/'/g,"\\'")}', '${this.selectedModel.replace(/'/g,"\\'")}')`}),this.selectedYear&&t.push({label:this.selectedYear}),t.push({label:"Categorías",action:`dashboard.goToCategories(${this.selectedVehicleId})`}),t.push({label:this.selectedCategory?this.selectedCategory.name_es||this.selectedCategory.name:"Grupos",active:!0});else if("parts"===this.currentView){const e=this.selectedGroup?this.selectedGroup.name_es||this.selectedGroup.name:"Grupo";t.push({label:' Marcas',action:"dashboard.goToBrands()"}),t.push({label:this.selectedBrand,action:`dashboard.goToModels('${this.selectedBrand.replace(/'/g,"\\'")}')`}),t.push({label:this.selectedModel,action:`dashboard.goToVehicles('${this.selectedBrand.replace(/'/g,"\\'")}', '${this.selectedModel.replace(/'/g,"\\'")}')`}),this.selectedYear&&t.push({label:this.selectedYear}),t.push({label:"Categorías",action:`dashboard.goToCategories(${this.selectedVehicleId})`}),t.push({label:this.selectedCategory?this.selectedCategory.name_es||this.selectedCategory.name:"Categoría",action:`dashboard.goToGroups(${this.selectedCategory?this.selectedCategory.id:0})`}),t.push({label:e,active:!0})}e.innerHTML=t.map(((e,a)=>{const s=a===t.length-1?"":'/';return e.action?`${e.label}${s}`:e.active?`${e.label}`:`${e.label}${s}`})).join("")}bindKeyboardShortcuts(){document.addEventListener("keydown",(e=>{const t="INPUT"===e.target.tagName||"TEXTAREA"===e.target.tagName;if("Escape"!==e.key){if(!t){if(!("/"===e.key||e.ctrlKey&&"k"===e.key))return e.ctrlKey&&"d"===e.key?(e.preventDefault(),void this.toggleDarkMode()):"Backspace"===e.key?(e.preventDefault(),void this.goBack()):void 0;{e.preventDefault();const t=document.getElementById("searchInput");t&&t.focus()}}}else this.closeAllModals()}))}closeAllModals(){["partDetailModal","searchResultsModal","diagramModal","vinDecoderModal"].forEach((e=>{this.closeModal(e)}))}openModal(e){this.lastFocusedElement=document.activeElement;const t=document.getElementById(e);t&&(t.classList.add("active"),setTimeout((()=>{const e=t.querySelector('input, button, [tabindex]:not([tabindex="-1"])');e&&e.focus()}),100))}closeModal(e){const t=document.getElementById(e);t&&(t.classList.remove("active"),this.lastFocusedElement&&this.lastFocusedElement.focus())}goBack(){switch(this.currentView){case"models":this.goToBrands();break;case"vehicles":this.goToModels(this.selectedBrand);break;case"categories":this.goToVehicles(this.selectedBrand,this.selectedModel);break;case"groups":this.goToCategories(this.selectedVehicleId);break;case"parts":this.selectedCategory?this.goToGroups(this.selectedCategory.id):this.goToCategories(this.selectedVehicleId)}}initDarkMode(){}toggleDarkMode(){}updateDarkModeIcon(){}makeCardsAccessible(e,t){const a=document.querySelector(e);if(!a)return;a.querySelectorAll(t).forEach(((e,t)=>{e.setAttribute("tabindex","0"),e.setAttribute("role","button");const a=e.textContent.trim().replace(/\s+/g," ");e.setAttribute("aria-label",a.substring(0,100)),e.addEventListener("keydown",(t=>{"Enter"!==t.key&&" "!==t.key||(t.preventDefault(),e.click())}))}))}openModalWithFocus(e){return this.openModal(e),{hide:()=>this.closeModal(e)}}async showBrands(){this.currentView="brands",this.selectedBrand=null,this.selectedModel=null,this.selectedYear=null,this.selectedGroup=null,this.updateBreadcrumb(),this.hideFilters();const e=document.getElementById("mainContent");e.innerHTML='\n
\n \n

Cargando marcas...

\n
\n ';try{const t=await fetch("/api/brands?detailed=true");if(!t.ok)throw new Error("Error al cargar datos");const a=await t.json(),s={};if(a.forEach((e=>{s[e.name]={models:{size:e.model_count},vehicles:e.vehicle_count}})),0===a.length)return void(e.innerHTML='\n
\n \n

No hay marcas disponibles

\n

Agrega algunas marcas a la base de datos

\n
\n ');e.innerHTML=`
\n ${a.map((e=>`\n
\n
\n \n
\n
${e.name}
\n
\n ${e.model_count} modelos\n
\n
\n ${e.vehicle_count} vehículos\n
\n
\n `)).join("")}\n
`,this.makeCardsAccessible("#mainContent",".brand-card")}catch(t){console.error("Error:",t),e.innerHTML=`\n
\n \n

Error al cargar marcas

\n

${t.message}

\n
\n `}}async goToModels(e){this.currentView="models",this.selectedBrand=e,this.selectedModel=null,this.selectedYear=null,this.selectedGroup=null,this.updateBreadcrumb(),this.hideFilters();const t=document.getElementById("mainContent");t.innerHTML=`\n
\n \n

Cargando modelos de ${e}...

\n
\n `;try{const a=await fetch(`/api/models?brand=${encodeURIComponent(e)}&detailed=true`);if(!a.ok)throw new Error("Error al cargar datos");const s=await a.json();if(0===s.length)return void(t.innerHTML=`\n
\n \n

No hay modelos para ${e}

\n

Esta marca no tiene modelos registrados

\n \n
\n `);t.innerHTML=`
\n ${s.map((t=>{const a=t.year_count>1?`${t.year_min} - ${t.year_max}`:`${t.year_min}`;return`\n
\n
${t.name}
\n
\n ${a}\n
\n
\n ${t.engine_count} motores\n
\n
\n ${t.vehicle_count} variantes\n
\n
\n `})).join("")}\n
`,this.makeCardsAccessible("#mainContent",".model-card")}catch(e){console.error("Error:",e),t.innerHTML=`\n
\n \n

Error al cargar modelos

\n

${e.message}

\n \n
\n `}}async goToVehicles(e,t){this.currentView="vehicles",this.selectedBrand=e,this.selectedModel=t,this.selectedYear=null,this.selectedGroup=null,this.updateBreadcrumb(),this.showFilters();const a=document.getElementById("mainContent");a.innerHTML='\n
\n \n

Cargando vehículos...

\n
\n ';try{const[a,s]=await Promise.all([fetch(`/api/vehicles?brand=${encodeURIComponent(e)}&model=${encodeURIComponent(t)}&per_page=100`),fetch(`/api/model-year-engine?brand=${encodeURIComponent(e)}&model=${encodeURIComponent(t)}&per_page=100`)]);if(!a.ok||!s.ok)throw new Error("Error al cargar vehículos");const n=await a.json(),i=await s.json(),r=n.data||n,o=i.data||i;this.allVehicles=r.map((e=>{const t=o.find((t=>t.brand===e.brand&&t.model===e.model&&t.year===e.year&&t.engine===e.engine));return{...e,mye_id:t?t.id:null}})).filter((e=>null!==e.mye_id)),this.filteredVehicles=[...this.allVehicles],await this.populateFilters(e,t),this.displayVehicles()}catch(t){console.error("Error:",t),a.innerHTML=`\n
\n \n

Error al cargar vehículos

\n

${t.message}

\n \n
\n `}}async populateFilters(e,t){try{const[a,s]=await Promise.all([fetch(`/api/years?brand=${encodeURIComponent(e)}&model=${encodeURIComponent(t)}`),fetch(`/api/engines?brand=${encodeURIComponent(e)}&model=${encodeURIComponent(t)}`)]);if(a.ok){const e=await a.json();document.getElementById("yearFilter").innerHTML=''+e.map((e=>``)).join("")}if(s.ok){const e=await s.json();document.getElementById("engineFilter").innerHTML=''+e.map((e=>``)).join("")}}catch(e){console.error("Error populating filters:",e)}}bindFilterEvents(){document.getElementById("yearFilter").addEventListener("change",(()=>{this.applyFilters()})),document.getElementById("engineFilter").addEventListener("change",(()=>{this.applyFilters()}))}applyFilters(){const e=document.getElementById("yearFilter").value,t=document.getElementById("engineFilter").value;this.filteredVehicles=this.allVehicles.filter((a=>!(e&&a.year.toString()!==e||t&&a.engine!==t))),this.displayVehicles()}getEngineConfig(e){if(!e)return"N/A";const t=e.toUpperCase(),a=t.match(/V(\d+)/);if(a)return`V${a[1]}`;const s=t.match(/I(\d+)|INLINE[- ]?(\d+)/);if(s)return`I${s[1]||s[2]}`;const n=t.match(/H(\d+)|FLAT[- ]?(\d+)/);if(n)return`H${n[1]||n[2]}`;const i=t.match(/W(\d+)/);if(i)return`W${i[1]}`;const r=t.match(/(\d)[- ]?CYL/);return r?`${r[1]} Cil`:t.includes("ELECTRIC")?"EV":t.includes("ROTARY")?"Rotary":"N/A"}formatDisplacement(e){if(!e||0===e)return"N/A";return`${(e/1e3).toFixed(1)}L`}formatFuelType(e){return{gasoline:"Gasolina",diesel:"Diésel",electric:"Eléctrico",hybrid:"Híbrido",other:"Otro"}[e]||e||"N/A"}displayVehicles(){const e=document.getElementById("mainContent");document.getElementById("resultCount").textContent=`${this.filteredVehicles.length} resultado${1!==this.filteredVehicles.length?"s":""}`,0!==this.filteredVehicles.length?e.innerHTML=`
\n ${this.filteredVehicles.map((e=>`\n
\n
\n
${e.year} ${e.brand} ${e.model}
\n
${e.engine}
\n
\n
\n
\n
\n \n
${this.formatFuelType(e.fuel_type)}
\n
\n
\n \n
${e.power_hp||"N/A"} HP
\n
\n
\n \n
${e.torque_nm||"N/A"} Nm
\n
\n
\n \n
${this.formatDisplacement(e.displacement_cc)}
\n
\n
\n \n
${e.cylinders||"N/A"} Cil
\n
\n
\n \n
${this.getEngineConfig(e.engine)}
\n
\n
\n ${e.trim_level&&"unknown"!==e.trim_level?`\n
\n ${e.trim_level}\n
\n `:""}\n \n
\n
\n `)).join("")}\n
`:e.innerHTML='\n
\n \n

No se encontraron vehículos

\n

Intenta ajustar los filtros

\n
\n '}goToBrands(){this.showBrands()}showFilters(){document.getElementById("filtersBar").classList.add("visible"),document.getElementById("yearFilter").value="",document.getElementById("engineFilter").value=""}hideFilters(){document.getElementById("filtersBar").classList.remove("visible")}async navigateToVehicle(e,t,a,s){this.selectedBrand=t,this.selectedModel=a,this.selectedYear=s,this.allVehicles.find((t=>t.mye_id===e))||this.allVehicles.push({mye_id:e,brand:t,model:a,year:s}),await this.goToCategories(e)}async navigateToVehicleCategory(e,t,a,s,n){if(this.selectedBrand=t,this.selectedModel=a,this.selectedYear=s,this.allVehicles.find((t=>t.mye_id===e))||this.allVehicles.push({mye_id:e,brand:t,model:a,year:s}),this.selectedVehicleId=e,0===this.allCategories.length)try{const e=await fetch("/api/categories");e.ok&&(this.allCategories=await e.json())}catch(e){console.error("Error loading categories:",e)}await this.goToGroups(n)}async goToCategories(e){if(!e||"null"===e||"undefined"===e){return void(document.getElementById("mainContent").innerHTML=`\n
\n \n

Vehículo sin partes disponibles

\n

Este vehículo no tiene partes registradas en el catálogo.

\n \n
\n `)}this.currentView="categories",this.selectedVehicleId=e,this.selectedCategory=null;const t=this.allVehicles.find((t=>t.mye_id===e));t&&(this.selectedYear=t.year),this.updateBreadcrumb(),this.hideFilters();const a=document.getElementById("mainContent");a.innerHTML='\n
\n \n

Cargando categorías...

\n
\n ';try{const t=await fetch(`/api/vehicles/${e}/categories`);if(!t.ok)throw new Error("Error al cargar categorías");this.allCategories=await t.json(),this.displayCategories()}catch(e){console.error("Error:",e),a.innerHTML=`\n
\n \n

Error al cargar categorías

\n

${e.message}

\n \n
\n `}}displayCategories(){const e=document.getElementById("mainContent");0!==this.allCategories.length?(e.innerHTML=`
\n ${this.allCategories.map((e=>{const t=e.icon_name||"fa-cog",a=e.name_es||e.name;return`\n
\n
\n \n
\n
${a}
\n
\n ${e.children?e.children.length+" subcategorías":""}\n
\n
\n `})).join("")}\n
\n
\n \n
`,this.makeCardsAccessible("#mainContent",".category-card")):e.innerHTML=`\n
\n \n

No hay categorías disponibles

\n

Este vehículo no tiene partes registradas

\n \n
\n `}async goToGroups(e){this.currentView="groups";const t=this.allCategories.find((t=>t.id===e));this.selectedCategory=t||{id:e,name:"Categoria"},this.selectedGroup=null,this.updateBreadcrumb(),this.hideFilters();const a=document.getElementById("mainContent");a.innerHTML='\n
\n \n

Cargando grupos...

\n
\n ';try{let t;t=this.selectedVehicleId?`/api/vehicles/${this.selectedVehicleId}/groups?category_id=${e}`:`/api/categories/${e}/groups`;const a=await fetch(t);if(!a.ok)throw new Error("Error al cargar grupos");const s=await a.json();let n=[];if(this.selectedVehicleId&&(10===e||11===e))try{const t=await fetch(`/api/vehicles/${this.selectedVehicleId}/diagrams/by-category?category_id=${e}`);if(t.ok){const e=await t.json();for(const t of e)n.push(...t.diagrams)}}catch(e){console.error("Error loading diagrams for strip:",e)}this.displayGroups(s,e,n)}catch(e){console.error("Error:",e),a.innerHTML=`\n
\n \n

Error al cargar grupos

\n

${e.message}

\n \n
\n `}}displayGroups(e,t,a=[]){const s=document.getElementById("mainContent");if(0===e.length&&0===a.length)return void(s.innerHTML=`\n
\n \n

No hay grupos en esta categoría

\n \n
\n `);let n="";a.length>0&&(this._currentDiagramList=a,n=`\n
\n
\n
Diagramas MOOG para tu vehículo
\n ${a.length} diagrama${1!==a.length?"s":""}\n
\n
\n ${a.map(((e,t)=>{const a=(e.name||"")[0],s="F"===a?"Delantera":"S"===a?"Dirección":"R"===a?"Trasera":"",n=e.image_url||"/static/diagrams/moog/"+e.name+".jpg";return`\n
\n ${e.name}\n
\n
${e.name}
\n
${s}
\n
\n
`})).join("")}\n
\n
`),s.innerHTML=`\n

${this.selectedCategory.name_es||this.selectedCategory.name}

\n ${n}\n
\n ${e.map((e=>`\n
\n
\n \n
\n
${e.name_es||e.name}
\n
\n \n \n
\n
\n `)).join("")}\n
\n
\n \n
`,this.makeCardsAccessible("#mainContent",".category-card")}async goToParts(e){this.currentView="parts",this.selectedGroupId=e;try{const t=await fetch(`/api/categories/${this.selectedCategory?this.selectedCategory.id:0}/groups`);if(t.ok){const a=await t.json();this.selectedGroup=a.find((t=>t.id===e))||{id:e,name:"Grupo"}}}catch(t){console.error("Error fetching group details:",t),this.selectedGroup={id:e,name:"Grupo"}}this.updateBreadcrumb(),this.hideFilters();const t=document.getElementById("mainContent");t.innerHTML='\n
\n \n

Cargando partes...

\n
\n ';try{let t;t=this.selectedVehicleId?`/api/vehicles/${this.selectedVehicleId}/parts?group_id=${e}`:`/api/parts?group_id=${e}`;const a=await fetch(t);if(!a.ok)throw new Error("Error al cargar partes");const s=await a.json();this.allParts=Array.isArray(s)?s:s.data||s,this.displayParts()}catch(e){console.error("Error:",e),t.innerHTML=`\n
\n \n

Error al cargar partes

\n

${e.message}

\n \n
\n `}}displayParts(){const e=document.getElementById("mainContent");0!==this.allParts.length?e.innerHTML=`\n
\n \n \n \n \n \n \n \n \n \n \n ${this.allParts.map((e=>`\n \n \n \n \n \n \n `)).join("")}\n \n
OEM # Nombre Grupo Acción
${e.oem_part_number||"N/A"}${e.name_es||e.name||"Sin nombre"}${e.group_name||"N/A"}\n \n
\n
\n
\n \n
\n `:e.innerHTML=`\n
\n \n

No hay partes disponibles

\n

Este grupo no tiene partes registradas aún

\n \n
\n `}async showPartDetail(e){const t=document.getElementById("partDetailContent");t.innerHTML='\n
\n \n

Cargando detalles...

\n
\n ';this.openModalWithFocus("partDetailModal");try{const[a,s,n]=await Promise.all([fetch(`/api/parts/${e}`),fetch(`/api/parts/${e}/alternatives`),fetch(`/api/parts/${e}/cross-references`)]);if(!a.ok)throw new Error("Error al cargar detalle de la parte");const i=await a.json(),r=s.ok?await s.json():[],o=n.ok?await n.json():[];t.innerHTML=`\n
\n
\n

${i.name_es||i.name||"Sin nombre"}

\n
\n
\n ${i.image_url?`\n
\n ${i.oem_part_number||\n
\n `:""}\n
\n Número OEM\n ${i.oem_part_number||"N/A"}\n
\n
\n Categoría\n ${i.category_name_es||i.category_name||"N/A"}\n
\n
\n Grupo\n ${i.group_name_es||i.group_name||"N/A"}\n
\n ${i.description||i.description_es?`\n
\n Descripción\n ${i.description_es||i.description}\n
\n `:""}\n\n \x3c!-- FASE 2: Cross-Referencias Section --\x3e\n ${this.renderCrossReferences(o)}\n\n \x3c!-- FASE 2: Alternativas Aftermarket Section --\x3e\n ${this.renderAlternatives(r)}\n `}catch(e){console.error("Error:",e),t.innerHTML=`\n
\n \n

${e.message}

\n
\n `}}renderCrossReferences(e){if(!e||0===e.length)return"";return`\n
\n
Cross-Referencias
\n
\n ${e.map((e=>`${e.cross_reference_number||e.part_number||e}${e.brand?` (${e.brand})`:""}`)).join("")}\n
\n
\n `}renderAlternatives(e){if(!e||0===e.length)return"";return`\n
\n
Alternativas Aftermarket
\n \n \n \n \n \n \n \n \n \n \n \n ${e.map((e=>`\n \n \n \n \n \n \n \n `)).join("")}\n \n
MarcaNúmero de ParteNombreCalidadPrecio
${e.brand||"N/A"}${e.part_number||"N/A"}${e.name_es||e.name||"N/A"}${this.getQualityBadge(e.quality_tier)}${this.formatPrice(e.price)}
\n
\n `}getQualityBadge(e){const t={economy:{class:"quality-economy",label:"Económico"},standard:{class:"quality-standard",label:"Estándar"},premium:{class:"quality-premium",label:"Premium"},oem:{class:"quality-oem",label:"OEM"}},a=t[e?.toLowerCase()]||t.standard;return`${a.label}`}formatPrice(e){return null==e?"N/A":new Intl.NumberFormat("es-MX",{style:"currency",currency:"MXN"}).format(e)}async searchPartNumber(){const e=document.getElementById("searchInput").value.trim();if(!e)return;if(17===e.length&&/^[A-HJ-NPR-Z0-9]{17}$/i.test(e)&&confirm("El texto parece ser un VIN. ¿Deseas decodificarlo?"))return document.getElementById("vinInput").value=e.toUpperCase(),void this.openVinDecoder();const t=document.getElementById("searchResultsContent");t.innerHTML=`\n
\n \n

Buscando "${e}"...

\n
\n `;this.openModalWithFocus("searchResultsModal");try{let t;t=await fetch(`/api/search/part-number/${encodeURIComponent(e)}`);let a=[];if(t.ok&&(a=await t.json()),0===a.length){const t=await fetch(`/api/search?q=${encodeURIComponent(e)}`);if(t.ok){const e=await t.json();a=e.parts||e||[]}}this.showSearchResults(a,e)}catch(e){console.error("Error:",e),t.innerHTML=`\n
\n \n

Error al buscar: ${e.message}

\n
\n `}}showSearchResults(e,t){const a=document.getElementById("searchResultsContent");if(document.getElementById("searchResultsModalLabel").innerHTML=` Resultados para "${t}"`,!e||0===e.length)return void(a.innerHTML=`\n
\n \n

No se encontraron resultados para "${t}"

\n
\n `);const s=e.map((e=>`\n
\n
\n
\n
\n ${e.oem_part_number||e.part_number||"N/A"}\n
\n
${e.name_es||e.name||"Sin nombre"}
\n
\n
\n ${e.quality_tier?this.getQualityBadge(e.quality_tier):""}\n ${e.brand?`
${e.brand}
`:""}\n
\n
\n
\n `)).join("");a.innerHTML=`\n

${e.length} resultado${1!==e.length?"s":""} encontrado${1!==e.length?"s":""}

\n
\n ${s}\n
\n `,this.makeCardsAccessible("#searchResultsContent",".search-result-item")}showPartDetailFromSearch(e){const t=document.getElementById("searchResultsModal");t&&t.classList.remove("active"),setTimeout((()=>{this.showPartDetail(e)}),300)}async goToDiagrams(e){const t=document.getElementById("mainContent");t.innerHTML='\n
\n \n

Cargando diagramas...

\n
\n ';try{const t=await fetch(`/api/groups/${e}/diagrams`);if(!t.ok)throw new Error("Error al cargar diagramas");const a=await t.json();this.displayDiagramThumbnails(a,e)}catch(e){console.error("Error:",e),t.innerHTML=`\n
\n \n

Error al cargar diagramas

\n

${e.message}

\n \n
\n `}}displayDiagramThumbnails(e,t){const a=document.getElementById("mainContent");e&&0!==e.length?(a.innerHTML=`\n
\n
\n
Diagramas del Grupo
\n ${e.length} diagrama${1!==e.length?"s":""}\n
\n
\n ${e.map((e=>`\n
\n
\n ${e.thumbnail_url?`${e.name_es||e.name}`:''}\n
\n
${e.name_es||e.name||"Diagrama"}
\n
\n `)).join("")}\n
\n
\n
\n \n
\n `,this.makeCardsAccessible("#mainContent",".diagram-thumbnail")):a.innerHTML=`\n
\n \n

No hay diagramas disponibles

\n

Este grupo no tiene diagramas registrados

\n \n
\n `}async showDiagram(e){const t=document.getElementById("diagramModalContent"),a=document.getElementById("diagramModalTitle");t.innerHTML='\n
\n \n

Cargando diagrama...

\n
\n ';this.openModalWithFocus("diagramModal");try{const t=await fetch(`/api/diagrams/${e}`);if(!t.ok)throw new Error("Error al cargar diagrama");const s=await t.json();a.innerHTML=` ${s.name_es||s.name||"Diagrama"}`,this.currentDiagramZoom=1,this.renderDiagramWithHotspots(s)}catch(e){console.error("Error:",e),t.innerHTML=`\n
\n \n

Error al cargar diagrama: ${e.message}

\n
\n `}}renderDiagramWithHotspots(e){const t=document.getElementById("diagramModalContent"),a=e.hotspots||[];t.innerHTML=`\n
\n ${e.description_es||e.description||""}\n
\n \n \n \n
\n
\n
\n
\n ${e.svg_content?e.svg_content:e.image_url?`${e.name_es||e.name}`:'
\n \n

No hay imagen de diagrama disponible

\n
'}\n ${a.map(((e,t)=>this.renderHotspot(e,t))).join("")}\n
\n
\n ${a.length>0?`\n
\n
Leyenda de Partes
\n
\n ${a.map(((e,t)=>`\n
\n ${t+1}\n ${e.name_es||e.name||e.label||"Parte "+(t+1)}\n
\n `)).join("")}\n
\n
\n `:""}\n `}renderHotspot(e,t){const a=e.x||e.position_x||0,s=e.y||e.position_y||0,n=e.width||30,i=e.height||30;return`\n
\n
\n ${t+1}\n
\n \n \n \n
\n `}onHotspotClick(e){if(e.part_id){const t=document.getElementById("diagramModal");t&&t.classList.remove("active"),setTimeout((()=>{this.showPartDetail(e.part_id)}),300)}else{const t=e.name_es||e.name||e.label||"Parte",a=e.description_es||e.description||"";alert(`${t}${a?"\n\n"+a:""}`)}}zoomDiagram(e){const t=document.getElementById("diagramWrapper");t&&(this.currentDiagramZoom=0===e?1:Math.max(.5,Math.min(2,this.currentDiagramZoom+e)),t.style.transform=`scale(${this.currentDiagramZoom})`)}openDiagramViewer(e,t){this._dvCurrentIndex="number"==typeof t?t:-1,this._dvDiagramList=this._currentDiagramList||[],this._dvZoom=1,this._dvDragging=!1;document.getElementById("diagramViewerOverlay").classList.add("active"),document.body.style.overflow="hidden",this._loadDiagramInViewer(e),this._bindDiagramViewerEvents()}closeDiagramViewer(){document.getElementById("diagramViewerOverlay").classList.remove("active"),document.body.style.overflow="",this._unbindDiagramViewerEvents()}async _loadDiagramInViewer(e){const t=document.getElementById("dvTitle"),a=document.getElementById("dvSubtitle"),s=document.getElementById("dvImgWrapper"),n=document.getElementById("dvImg"),i=document.getElementById("dvPartsList"),r=document.getElementById("dvPartsCount");i.innerHTML='

Cargando...

',r.textContent="...";try{const[i,r]=await Promise.all([fetch(`/api/diagrams/${e}`),fetch(`/api/diagrams/${e}/parts${this.selectedVehicleId?"?mye_id="+this.selectedVehicleId:""}`)]),o=await i.json(),l=await r.json(),d=(o.name||"")[0],c="F"===d?"Suspensión Delantera":"S"===d?"Dirección":"R"===d?"Suspensión Trasera":o.group_name||"";t.textContent=o.name||"Diagrama",a.textContent=o.name_es||c;const m=o.image_url||(o.image_path?"/"+o.image_path:"");n.src=m,n.alt=o.name_es||o.name,this._dvZoom=1,s.style.transform="",s.classList.remove("zoomed"),document.getElementById("dvZoomLevel").textContent="100%",this._renderViewerHotspots(o.hotspots||[],s),this._renderViewerParts(l,o.hotspots||[]);const h=document.getElementById("dvPrevBtn"),p=document.getElementById("dvNextBtn");h.disabled=this._dvCurrentIndex<=0,p.disabled=this._dvCurrentIndex<0||this._dvCurrentIndex>=this._dvDiagramList.length-1,h.style.opacity=h.disabled?"0.3":"1",p.style.opacity=p.disabled?"0.3":"1"}catch(e){console.error("Error loading diagram in viewer:",e),i.innerHTML='

Error cargando diagrama

'}}_renderViewerHotspots(e,t){t.querySelectorAll(".hotspot-marker").forEach((e=>e.remove())),e&&0!==e.length&&e.forEach(((e,a)=>{const s=(e.coords||"").split(",");if(s.length<2)return;const n=parseFloat(s[0]),i=parseFloat(s[1]);if(isNaN(n)||isNaN(i))return;const r=document.createElement("div");r.className="hotspot-marker pulse",r.style.left=n+"%",r.style.top=i+"%",r.dataset.partId=e.part_id||"",r.dataset.callout=e.callout_number||a+1,r.title=e.part_name||e.label||"Parte "+(a+1),r.innerHTML=`${e.callout_number||a+1}`,r.addEventListener("click",(()=>{this._highlightPartInList(e.part_id),t.querySelectorAll(".hotspot-marker").forEach((e=>e.classList.remove("active"))),r.classList.add("active")})),t.appendChild(r)}))}_renderViewerParts(e,t){const a=document.getElementById("dvPartsList");if(document.getElementById("dvPartsCount").textContent=e.length,!e||0===e.length)return void(a.innerHTML='

No hay partes vinculadas

');const s={};(t||[]).forEach(((e,t)=>{e.part_id&&(s[e.part_id]=e.callout_number||t+1)}));const n={};e.forEach((e=>{const t=e.group_name_es||e.group_name||"Otros";n[t]||(n[t]=[]),n[t].push(e)}));let i="";for(const[e,t]of Object.entries(n)){i+=`
${e}
`;for(const e of t){const t=s[e.id];let a="";e.cross_references&&e.cross_references.length>0&&(a=`
${e.cross_references.map((e=>`${e.number}`)).join("")}
`),i+=`\n
\n
\n ${t?`${t}`:""}\n
${e.part_number||e.oem_part_number}
\n
\n
${e.name_es||e.name||""}
\n ${a}\n
`}}a.innerHTML=i}_highlightPartInList(e){if(!e)return;const t=document.getElementById("dvPartsList");t.querySelectorAll(".dv-part-item").forEach((e=>e.classList.remove("highlighted")));const a=t.querySelector(`.dv-part-item[data-part-id="${e}"]`);a&&(a.classList.add("highlighted"),a.scrollIntoView({behavior:"smooth",block:"nearest"}))}_onViewerPartClick(e){this._highlightPartInList(e);document.getElementById("dvImgWrapper").querySelectorAll(".hotspot-marker").forEach((t=>{t.classList.remove("active"),t.dataset.partId==e&&t.classList.add("active")}))}_dvNavigate(e){const t=this._dvCurrentIndex+e;if(t<0||t>=this._dvDiagramList.length)return;this._dvCurrentIndex=t;const a=this._dvDiagramList[t];a&&this._loadDiagramInViewer(a.id)}_dvSetZoom(e){this._dvZoom=Math.max(.25,Math.min(4,e));const t=document.getElementById("dvImgWrapper");1!==this._dvZoom?(t.classList.add("zoomed"),t.style.transform=`scale(${this._dvZoom})`):(t.classList.remove("zoomed"),t.style.transform=""),document.getElementById("dvZoomLevel").textContent=`${Math.round(100*this._dvZoom)}%`}_bindDiagramViewerEvents(){this._dvBound||(this._dvBound=!0,this._dvHandlers={close:()=>this.closeDiagramViewer(),prev:()=>this._dvNavigate(-1),next:()=>this._dvNavigate(1),zoomIn:()=>this._dvSetZoom(this._dvZoom+.25),zoomOut:()=>this._dvSetZoom(this._dvZoom-.25),zoomFit:()=>this._dvSetZoom(1),keydown:e=>{document.getElementById("diagramViewerOverlay").classList.contains("active")&&("Escape"===e.key&&this.closeDiagramViewer(),"ArrowLeft"===e.key&&this._dvNavigate(-1),"ArrowRight"===e.key&&this._dvNavigate(1),"+"!==e.key&&"="!==e.key||this._dvSetZoom(this._dvZoom+.25),"-"===e.key&&this._dvSetZoom(this._dvZoom-.25))},wheel:e=>{if(!document.getElementById("diagramViewerOverlay").classList.contains("active"))return;e.preventDefault();const t=e.deltaY>0?-.15:.15;this._dvSetZoom(this._dvZoom+t)},partsFilter:e=>{const t=e.target.value.toLowerCase();document.querySelectorAll("#dvPartsList .dv-part-item").forEach((e=>{e.style.display=e.textContent.toLowerCase().includes(t)?"":"none"}))},mousedown:e=>{if(this._dvZoom<=1)return;this._dvDragging=!0,this._dvDragStart={x:e.clientX,y:e.clientY};const t=document.getElementById("dvImgContainer");this._dvScrollStart={x:t.scrollLeft,y:t.scrollTop},t.style.cursor="grabbing"},mousemove:e=>{if(!this._dvDragging)return;const t=document.getElementById("dvImgContainer");t.scrollLeft=this._dvScrollStart.x-(e.clientX-this._dvDragStart.x),t.scrollTop=this._dvScrollStart.y-(e.clientY-this._dvDragStart.y)},mouseup:()=>{this._dvDragging=!1;const e=document.getElementById("dvImgContainer");e&&(e.style.cursor="")}},document.getElementById("dvCloseBtn").addEventListener("click",this._dvHandlers.close),document.getElementById("dvPrevBtn").addEventListener("click",this._dvHandlers.prev),document.getElementById("dvNextBtn").addEventListener("click",this._dvHandlers.next),document.getElementById("dvZoomIn").addEventListener("click",this._dvHandlers.zoomIn),document.getElementById("dvZoomOut").addEventListener("click",this._dvHandlers.zoomOut),document.getElementById("dvZoomFit").addEventListener("click",this._dvHandlers.zoomFit),document.getElementById("dvPartsFilter").addEventListener("input",this._dvHandlers.partsFilter),document.addEventListener("keydown",this._dvHandlers.keydown),document.getElementById("dvImgContainer").addEventListener("wheel",this._dvHandlers.wheel,{passive:!1}),document.getElementById("dvImgContainer").addEventListener("mousedown",this._dvHandlers.mousedown),window.addEventListener("mousemove",this._dvHandlers.mousemove),window.addEventListener("mouseup",this._dvHandlers.mouseup))}_unbindDiagramViewerEvents(){this._dvBound&&(this._dvBound=!1,document.getElementById("dvCloseBtn")?.removeEventListener("click",this._dvHandlers.close),document.getElementById("dvPrevBtn")?.removeEventListener("click",this._dvHandlers.prev),document.getElementById("dvNextBtn")?.removeEventListener("click",this._dvHandlers.next),document.getElementById("dvZoomIn")?.removeEventListener("click",this._dvHandlers.zoomIn),document.getElementById("dvZoomOut")?.removeEventListener("click",this._dvHandlers.zoomOut),document.getElementById("dvZoomFit")?.removeEventListener("click",this._dvHandlers.zoomFit),document.getElementById("dvPartsFilter")?.removeEventListener("input",this._dvHandlers.partsFilter),document.removeEventListener("keydown",this._dvHandlers.keydown),document.getElementById("dvImgContainer")?.removeEventListener("wheel",this._dvHandlers.wheel),document.getElementById("dvImgContainer")?.removeEventListener("mousedown",this._dvHandlers.mousedown),window.removeEventListener("mousemove",this._dvHandlers.mousemove),window.removeEventListener("mouseup",this._dvHandlers.mouseup))}openVinDecoder(){document.getElementById("vinResult").innerHTML="",this.openModalWithFocus("vinDecoderModal")}async decodeVin(){const e=document.getElementById("vinInput").value.trim().toUpperCase(),t=document.getElementById("vinResult");if(e)if(17===e.length)if(/[IOQ]/i.test(e))t.innerHTML='\n
\n El VIN contiene caracteres invalidos. Las letras I, O y Q no se permiten en VINs.\n
\n ';else if(/^[A-HJ-NPR-Z0-9]{17}$/i.test(e)){t.innerHTML='\n
\n \n

Decodificando VIN...

\n
\n ';try{const t=await fetch(`/api/vin/decode/${encodeURIComponent(e)}`);if(!t.ok){const e=await t.json().catch((()=>({})));throw new Error(e.detail||e.message||"Error al decodificar VIN")}const a=await t.json();this.showVinResult(a,e)}catch(e){console.error("Error:",e),t.innerHTML=`\n
\n ${e.message}\n
\n `}}else t.innerHTML='\n
\n El VIN contiene caracteres invalidos. Solo se permiten letras (excepto I, O, Q) y numeros.\n
\n ';else t.innerHTML=`\n
\n El VIN debe tener exactamente 17 caracteres (actual: ${e.length})\n
\n `;else t.innerHTML='\n
\n Por favor ingresa un VIN\n
\n '}showVinResult(e,t){const a=document.getElementById("vinResult"),s=e.vehicle||e,n=s.make||s.brand||"Desconocido",i=s.model||"Desconocido",r=s.year||s.model_year||"Desconocido",o=s.engine||s.engine_description||"N/A",l=s.trim||s.trim_level||"",d=(s.body_type||s.body_class,s.drive_type||s.drivetrain||"N/A"),c=s.fuel_type||"N/A",m=s.transmission||"N/A",h=s.country||s.plant_country||"N/A",p=e.matched||e.database_match||s.mye_id,g=s.mye_id||e.mye_id;let v="";v=p&&g?`\n
\n
\n
\n \n Vehiculo encontrado en la base de datos\n
\n \n
\n
\n `:`\n
\n
\n
\n \n Vehiculo no encontrado en la base de datos\n
\n \n
\n
\n `,a.innerHTML=`\n
\n
\n ${t}\n VIN Valido\n
\n\n
${r} ${n} ${i} ${l}
\n\n
\n
\n
\n Marca\n ${n}\n
\n
\n Modelo\n ${i}\n
\n
\n Año\n ${r}\n
\n
\n Motor\n ${o}\n
\n
\n
\n
\n Combustible\n ${c}\n
\n
\n Transmision\n ${m}\n
\n
\n Traccion\n ${d}\n
\n
\n Pais\n ${h}\n
\n
\n
\n\n ${v}\n
\n `}async viewVinParts(e,t){const a=document.getElementById("vinDecoderModal");a&&a.classList.remove("active");const s=document.getElementById("searchResultsContent");document.getElementById("searchResultsModalLabel").innerHTML=` Partes para VIN: ${e.substring(0,8)}...`,s.innerHTML='\n
\n \n

Cargando partes...

\n
\n ';this.openModalWithFocus("searchResultsModal");try{const a=await fetch(`/api/vin/${encodeURIComponent(e)}/parts`);if(!a.ok)throw new Error("Error al cargar partes");const s=await a.json(),n=s.parts||s;this.displayVinParts(n,e,t)}catch(e){console.error("Error:",e),s.innerHTML=t?`\n
\n No se encontraron partes especificas para este VIN.\n

\n \n
\n `:`\n
\n \n

Error al cargar partes: ${e.message}

\n
\n `}}displayVinParts(e,t,a){const s=document.getElementById("searchResultsContent");if(!e||0===e.length)return void(s.innerHTML=`\n
\n \n

No se encontraron partes para este VIN

\n ${a?`\n \n `:""}\n
\n `);const n={};e.forEach((e=>{const t=e.category_name_es||e.category_name||"Sin Categoria";n[t]||(n[t]=[]),n[t].push(e)}));let i=`

${e.length} parte${1!==e.length?"s":""} encontrada${1!==e.length?"s":""}

`;for(const[e,t]of Object.entries(n))i+=`\n
\n
${e}
\n
\n ${t.map((e=>`\n
\n
\n
\n
\n ${e.oem_part_number||e.part_number||"N/A"}\n
\n
${e.name_es||e.name||"Sin nombre"}
\n
\n
\n ${e.quality_tier?this.getQualityBadge(e.quality_tier):""}\n ${e.group_name_es||e.group_name?`
${e.group_name_es||e.group_name}
`:""}\n
\n
\n
\n `)).join("")}\n
\n
\n `;a&&(i+=`\n
\n \n
\n `),s.innerHTML=i}searchManuallyFromVin(e,t,a){const s=bootstrap.Modal.getInstance(document.getElementById("vinDecoderModal"));s&&s.hide(),setTimeout((async()=>{try{const t=await fetch("/api/brands");if(t.ok){const a=(await t.json()).find((t=>t.toLowerCase()===e.toLowerCase()||t.toLowerCase().includes(e.toLowerCase())||e.toLowerCase().includes(t.toLowerCase())));if(a)return void this.goToModels(a)}alert(`La marca "${e}" no se encontro en la base de datos. Mostrando todas las marcas disponibles.`),this.goToBrands()}catch(e){console.error("Error:",e),this.goToBrands()}}),300)}}let dashboard;document.addEventListener("DOMContentLoaded",(()=>{dashboard=new VehicleDashboard})); \ No newline at end of file diff --git a/dashboard/enhanced-search.min.js b/dashboard/enhanced-search.min.js new file mode 100644 index 0000000..258eb5c --- /dev/null +++ b/dashboard/enhanced-search.min.js @@ -0,0 +1 @@ +const enhancedSearch={config:{minChars:2,debounceMs:300,maxResults:8,maxRecent:5,storageKey:"nexus_recent_searches"},state:{query:"",results:{parts:[],vehicles:[]},highlightedIndex:-1,isOpen:!1,isLoading:!1,filtersVisible:!1,debounceTimer:null},elements:{},init(){this.cacheElements(),this.loadCategories(),this.renderRecentSearches(),this.setupClickOutside()},cacheElements(){this.elements={input:document.getElementById("searchInput"),dropdown:document.getElementById("searchDropdown"),loading:document.getElementById("searchLoading"),filters:document.getElementById("searchFilters"),recent:document.getElementById("searchRecent"),recentItems:document.getElementById("searchRecentItems"),resultsContainer:document.getElementById("searchResultsContainer"),partsResults:document.getElementById("partsResults"),partsResultsList:document.getElementById("partsResultsList"),vehiclesResults:document.getElementById("vehiclesResults"),vehiclesResultsList:document.getElementById("vehiclesResultsList"),noResults:document.getElementById("searchNoResults"),footer:document.getElementById("searchFooter"),categoryFilter:document.getElementById("searchCategoryFilter"),typeFilter:document.getElementById("searchTypeFilter")}},async loadCategories(){try{const e=await fetch("/api/categories"),t=await e.json();this.elements.categoryFilter&&(this.elements.categoryFilter.innerHTML=''+this.flattenCategories(t).map((e=>``)).join(""))}catch(e){console.error("Error loading categories:",e)}},flattenCategories(e,t=[]){return e.forEach((e=>{t.push(e),e.children&&e.children.length&&this.flattenCategories(e.children,t)})),t},onInput(e){this.state.query=e.trim(),this.state.debounceTimer&&clearTimeout(this.state.debounceTimer),this.state.query.length{this.performSearch()}),this.config.debounceMs)},onKeydown(e){const t=this.getAllResultItems();switch(e.key){case"ArrowDown":e.preventDefault(),this.highlightNext(t);break;case"ArrowUp":e.preventDefault(),this.highlightPrevious(t);break;case"Enter":e.preventDefault(),this.state.highlightedIndex>=0&&t[this.state.highlightedIndex]?t[this.state.highlightedIndex].click():this.state.query.length>=this.config.minChars&&this.viewAllResults();break;case"Escape":this.close(),this.elements.input.blur();break;case"Tab":if(this.state.isOpen&&this.state.highlightedIndex>=0&&t[this.state.highlightedIndex]){e.preventDefault();const s=t[this.state.highlightedIndex].dataset.autocomplete;s&&this.elements.input&&(this.elements.input.value=s,this.state.query=s,this.performSearch())}}},onFocus(){this.state.query.length>=this.config.minChars?this.open():this.showRecent()},getAllResultItems(){return Array.from(this.elements.dropdown.querySelectorAll(".search-result-item"))},highlightNext(e){0!==e.length&&(this.state.highlightedIndex++,this.state.highlightedIndex>=e.length&&(this.state.highlightedIndex=0),this.updateHighlight(e))},highlightPrevious(e){0!==e.length&&(this.state.highlightedIndex--,this.state.highlightedIndex<0&&(this.state.highlightedIndex=e.length-1),this.updateHighlight(e))},updateHighlight(e){e.forEach(((e,t)=>{e.classList.toggle("highlighted",t===this.state.highlightedIndex),t===this.state.highlightedIndex&&e.scrollIntoView({block:"nearest"})}))},async performSearch(){const e=this.state.query,t=this.elements.categoryFilter?.value,s=this.elements.typeFilter?.value||"all";this.showLoading(!0),this.open();try{let i=`/api/search?q=${encodeURIComponent(e)}&limit=${this.config.maxResults}`;t&&(i+=`&category_id=${t}`),"all"!==s&&(i+=`&type=${s}`);const a=await fetch(i),n=await a.json();this.state.results={parts:n.parts||[],vehicles:n.vehicles||[],vehicleParts:n.vehicle_parts||[],matchedVehicle:n.matched_vehicle||null},this.renderResults()}catch(e){console.error("Search error:",e),this.showNoResults()}finally{this.showLoading(!1)}},applyFilters(){this.state.query.length>=this.config.minChars&&this.performSearch()},renderResults(){const{parts:e,vehicles:t,vehicleParts:s,matchedVehicle:i}=this.state.results,a=s&&s.length>0,n=e.length>0||t.length>0||a;if(this.elements.recent&&(this.elements.recent.style.display="none"),a&&i&&this.elements.partsResults&&this.elements.partsResultsList){this.elements.partsResults.style.display="block";const e=`\n
\n \n ${i.brand} ${i.model} ${i.year}\n ${i.engine}\n
\n `;this.elements.partsResultsList.innerHTML=e+s.map(((e,t)=>this.renderVehiclePartItem(e,i,t))).join(""),this.elements.vehiclesResults&&(this.elements.vehiclesResults.style.display="none")}else e.length>0&&this.elements.partsResults&&this.elements.partsResultsList?(this.elements.partsResults.style.display="block",this.elements.partsResultsList.innerHTML=e.map(((e,t)=>this.renderPartItem(e,t))).join("")):this.elements.partsResults&&(this.elements.partsResults.style.display="none"),t.length>0&&this.elements.vehiclesResults&&this.elements.vehiclesResultsList?(this.elements.vehiclesResults.style.display="block",this.elements.vehiclesResultsList.innerHTML=t.map(((t,s)=>this.renderVehicleItem(t,e.length+s))).join("")):this.elements.vehiclesResults&&(this.elements.vehiclesResults.style.display="none");if(this.elements.noResults&&(this.elements.noResults.style.display=n?"none":"block"),this.elements.footer&&(this.elements.footer.style.display=n?"flex":"none"),n){this.state.highlightedIndex=0;const e=this.elements.dropdown.querySelector(".search-result-item");e&&e.classList.add("highlighted")}else this.state.highlightedIndex=-1},renderPartItem(e,t){const s=this.highlightText(e.name,this.state.query),i=e.matched_number||e.oem_part_number,a=this.highlightText(i,this.state.query),n=e.category_name?`${e.category_name}`:"",l=e.match_type&&"oem"!==e.match_type?`${{aftermarket:"Aftermarket",cross_reference:"Cross-Ref"}[e.match_type]||e.match_type}`:"",r=e.oem_part_number||e.name;return`\n
\n
\n ${e.image_url?``:''}\n
\n
\n
${s}
\n
\n ${a}\n ${n}\n
\n
\n ${l}\n
\n `},renderVehiclePartItem(e,t,s){const i=this.highlightText(e.name_es||e.name,this.state.query),a=this.highlightText(e.oem_part_number,this.state.query),n=e.group_name?`${e.group_name}`:"",l=JSON.stringify({id:t.id,brand:t.brand,model:t.model,year:t.year}).replace(/'/g,"\\'").replace(/"/g,"""),r=`${t.brand} ${t.model} ${t.year} ${e.name}`;return`\n
\n
\n ${e.image_url?``:''}\n
\n
\n
${i}
\n
\n ${a}\n ${n}\n
\n
\n \n Compatible\n \n
\n `},renderVehicleItem(e,t){const s=`${e.brand} ${e.model} ${e.year}`,i=e.engine||"",a=JSON.stringify({id:e.id,brand:e.brand,model:e.model,year:e.year}).replace(/'/g,"\\'").replace(/"/g,"""),n=[{id:6,icon:"fa-cog",name:"Motor"},{id:2,icon:"fa-compact-disc",name:"Frenos"},{id:5,icon:"fa-bolt",name:"Eléctrico"},{id:11,icon:"fa-truck-monster",name:"Suspensión"},{id:8,icon:"fa-gas-pump",name:"Combustible"},{id:12,icon:"fa-gears",name:"Transmisión"}].map((e=>``)).join(""),l=`${e.brand} ${e.model} ${e.year}`;return`\n
\n
\n
\n
${this.highlightText(s,this.state.query)}
\n
${this.highlightText(i,this.state.query)}
\n
\n ${n}\n \n
\n
\n
\n `},highlightText(e,t){if(!t||!e)return e;const s=new RegExp(`(${this.escapeRegex(t)})`,"gi");return e.replace(s,'$1')},escapeRegex:e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),escapeHtml:e=>e.replace(/'/g,"\\'").replace(/"/g,'\\"'),selectPart(e,t){this.saveRecentSearch(t),this.close(),"undefined"!=typeof dashboard&&"function"==typeof dashboard.showPartDetail?dashboard.showPartDetail(e):window.location.href=`/?search=${encodeURIComponent(t)}`},selectVehiclePart(e,t,s,i){try{const i=JSON.parse(e.replace(/"/g,'"'));i.brand,i.model,i.year;this.saveRecentSearch(this.state.query),this.close(),"undefined"!=typeof dashboard&&"function"==typeof dashboard.navigateToVehicleCategory&&(dashboard.navigateToVehicleCategory(i.id,i.brand,i.model,i.year,s),setTimeout((()=>{"function"==typeof dashboard.showPartDetail&&dashboard.showPartDetail(t)}),500))}catch(e){console.error("Error selecting vehicle part:",e)}},selectVehicle(e){try{const t=JSON.parse(e.replace(/"/g,'"')),s=`${t.brand} ${t.model} ${t.year}`;this.saveRecentSearch(s),this.close(),"undefined"!=typeof dashboard&&"function"==typeof dashboard.navigateToVehicle?dashboard.navigateToVehicle(t.id,t.brand,t.model,t.year):console.log("Navigating to vehicle:",t)}catch(e){console.error("Error parsing vehicle data:",e)}},selectVehicleCategory(e,t){try{const s=JSON.parse(e.replace(/"/g,'"')),i=`${s.brand} ${s.model} ${s.year}`;this.saveRecentSearch(i),this.close(),"undefined"!=typeof dashboard&&"function"==typeof dashboard.navigateToVehicleCategory?dashboard.navigateToVehicleCategory(s.id,s.brand,s.model,s.year,t):console.log("Navigating to vehicle category:",s,t)}catch(e){console.error("Error parsing vehicle data:",e)}},viewAllResults(){this.state.query&&(this.saveRecentSearch(this.state.query),this.close(),"undefined"!=typeof dashboard&&"function"==typeof dashboard.searchPartNumber?dashboard.searchPartNumber():console.log("Dashboard not available, search query:",this.state.query))},getRecentSearches(){try{return JSON.parse(localStorage.getItem(this.config.storageKey))||[]}catch{return[]}},saveRecentSearch(e){if(!e||e.length<2)return;let t=this.getRecentSearches();t=t.filter((t=>t.toLowerCase()!==e.toLowerCase())),t.unshift(e),t=t.slice(0,this.config.maxRecent),localStorage.setItem(this.config.storageKey,JSON.stringify(t)),this.renderRecentSearches()},clearRecent(){localStorage.removeItem(this.config.storageKey),this.renderRecentSearches()},renderRecentSearches(){const e=this.getRecentSearches();0!==e.length&&this.elements.recent?this.elements.recentItems&&(this.elements.recentItems.innerHTML=e.map((e=>`${e}`)).join("")):this.elements.recent&&(this.elements.recent.style.display="none")},searchRecent(e){this.elements.input&&(this.elements.input.value=e),this.state.query=e,this.performSearch()},showRecent(){this.getRecentSearches().length>0&&this.elements.recent?(this.elements.recent.style.display="block",this.elements.partsResults&&(this.elements.partsResults.style.display="none"),this.elements.vehiclesResults&&(this.elements.vehiclesResults.style.display="none"),this.elements.noResults&&(this.elements.noResults.style.display="none"),this.elements.footer&&(this.elements.footer.style.display="none"),this.open()):this.close()},toggleFilters(){this.state.filtersVisible=!this.state.filtersVisible,this.elements.filters&&(this.elements.filters.style.display=this.state.filtersVisible?"flex":"none");const e=document.querySelector(".search-filters-toggle");e&&e.classList.toggle("active",this.state.filtersVisible)},open(){this.state.isOpen=!0,this.elements.dropdown&&this.elements.dropdown.classList.add("active")},close(){this.state.isOpen=!1,this.elements.dropdown&&this.elements.dropdown.classList.remove("active"),this.state.highlightedIndex=-1},showLoading(e){this.state.isLoading=e,this.elements.loading&&(this.elements.loading.style.display=e?"block":"none")},showNoResults(){this.elements.partsResults&&(this.elements.partsResults.style.display="none"),this.elements.vehiclesResults&&(this.elements.vehiclesResults.style.display="none"),this.elements.noResults&&(this.elements.noResults.style.display="block"),this.elements.footer&&(this.elements.footer.style.display="none"),this.elements.recent&&(this.elements.recent.style.display="none")},setupClickOutside(){document.addEventListener("click",(e=>{const t=document.querySelector(".search-box-enhanced");t&&!t.contains(e.target)&&this.close()}))}};document.addEventListener("DOMContentLoaded",(()=>{enhancedSearch.init()})),document.addEventListener("keydown",(e=>{if("/"===e.key&&!["INPUT","TEXTAREA","SELECT"].includes(document.activeElement.tagName)){e.preventDefault();const t=document.getElementById("searchInput");t&&(t.focus(),t.select())}if((e.ctrlKey||e.metaKey)&&"k"===e.key){e.preventDefault();const t=document.getElementById("searchInput");t&&(t.focus(),t.select())}})); \ No newline at end of file diff --git a/dashboard/landing.min.js b/dashboard/landing.min.js new file mode 100644 index 0000000..734e560 --- /dev/null +++ b/dashboard/landing.min.js @@ -0,0 +1 @@ +!function(){"use strict";var t=document.getElementById("themeIcon");function e(){var e=document.documentElement.getAttribute("data-theme");t&&(t.innerHTML="industrial"===e?"☾":"☀")}window.toggleTheme=function(){var t=document.documentElement,n="industrial"===t.getAttribute("data-theme")?"modern":"industrial";t.setAttribute("data-theme",n),localStorage.setItem("nexus-theme",n),e()},e();var n=document.getElementById("heroCanvas");if(n){var a,r=n.getContext("2d"),o=80,i=[];function w(){var t=n.parentElement;n.width=t.offsetWidth,n.height=t.offsetHeight}function x(t){return getComputedStyle(document.documentElement).getPropertyValue(t).trim()}function y(t,e,n,a,o){r.beginPath();for(var i=0;i<4;i++){var l=Math.PI/2*i;r.lineTo(t+Math.cos(l)*n,e+Math.sin(l)*n),r.lineTo(t+Math.cos(l+Math.PI/4)*a,e+Math.sin(l+Math.PI/4)*a)}r.closePath(),r.fillStyle=o,r.fill()}function A(){var t=Math.floor(n.width/o),e=Math.floor(n.height/o);i.push({col:Math.floor(Math.random()*t),row:Math.floor(Math.random()*e),life:0,maxLife:90+60*Math.random()})}function I(){r.clearRect(0,0,n.width,n.height);var t=x("--canvas-grid-color")||"rgba(255,255,255,0.06)",e=x("--canvas-star-color")||"rgba(245,166,35,0.3)",a=x("--canvas-glow-color")||"rgba(245,166,35,0.08)",l=Math.floor(n.width/o)+1,c=Math.floor(n.height/o)+1;r.strokeStyle=t,r.lineWidth=1;for(var s=0;s<=l;s++){var u=s*o;r.beginPath(),r.moveTo(u,0),r.lineTo(u,n.height),r.stroke()}for(var d=0;d<=c;d++){var f=d*o;r.beginPath(),r.moveTo(0,f),r.lineTo(n.width,f),r.stroke()}for(s=0;s<=l;s+=2)for(d=0;d<=c;d+=2)y(s*o,d*o,6,3,e);i.length<6&&Math.random()<.03&&A();for(var h=i.length-1;h>=0;h--){var m=i[h];m.life++;var v=m.life/m.maxLife,g=v<.5?2*v:2*(1-v);r.fillStyle=a,r.globalAlpha=g,r.fillRect(m.col*o+1,m.row*o+1,78,78),r.shadowColor=a,r.shadowBlur=20,r.fillRect(m.col*o+1,m.row*o+1,78,78),r.shadowBlur=0,r.globalAlpha=1,m.life>=m.maxLife&&i.splice(h,1)}requestAnimationFrame(I)}w(),I(),window.addEventListener("resize",(function(){clearTimeout(a),a=setTimeout(w,150)}))}var l=document.querySelectorAll(".nx-reveal, .nx-reveal-scale");if("IntersectionObserver"in window){var c=new IntersectionObserver((function(t){t.forEach((function(t){t.isIntersecting&&(t.target.classList.add("is-visible"),c.unobserve(t.target))}))}),{threshold:.15,rootMargin:"0px 0px -40px 0px"});l.forEach((function(t){c.observe(t)}))}else l.forEach((function(t){t.classList.add("is-visible")}));var s=document.querySelectorAll(".stat-card");if(s.length&&"IntersectionObserver"in window){var u=new IntersectionObserver((function(t){t.forEach((function(t){if(t.isIntersecting){var e=t.target.querySelector(".number");if(e&&!e._animated){e._animated=!0;var n=parseInt(e.getAttribute("data-target"),10)||0,a=e.getAttribute("data-suffix")||"",r=e.getAttribute("data-format")||"num";!function(t,e,n,a,r){var o=null;requestAnimationFrame((function i(l){o||(o=l);var c=Math.min((l-o)/r,1),s=1-Math.pow(1-c,3),u=Math.floor(s*e);-1!==n.indexOf("M")?t.textContent=(u/1e6).toFixed(1)+"M"+a:-1!==n.indexOf("K")?t.textContent=Math.floor(u/1e3)+"K"+a:t.textContent=u+a,c<1&&requestAnimationFrame(i)}))}(e,n,r,a,2e3)}u.unobserve(t.target)}}))}),{threshold:.5});s.forEach((function(t){u.observe(t)}))}var d=document.getElementById("typewriterText");if(d){var f=["POS + Inventario + CFDI 4.0 + Contabilidad","Catalogo TecDoc: 1.5M+ partes, 304K aftermarket","15.8M cross-references OEM ↔ aftermarket","Chatbot IA con voz, foto y diagnostico","WhatsApp Business integrado","Busca por VIN, placas o numero de parte","Marketplace B2B: bodegas ↔ talleres","PWA + Android + modo kiosko + offline"],h=0,m=0,v=!1;function k(){var t=f[h];if(v){if(d.textContent=t.substring(0,m),--m<0)return v=!1,m=0,h=(h+1)%f.length,void setTimeout(k,400);setTimeout(k,30)}else{if(d.textContent=t.substring(0,m+1),++m>=t.length)return v=!0,void setTimeout(k,2e3);setTimeout(k,50)}}setTimeout(k,1200)}var g=document.getElementById("brandsMarquee");function b(t){var e="";t.forEach((function(t){var n,a;e+=''+(n=t.name_brand,(a=document.createElement("div")).textContent=n||"",a.innerHTML+"")})),g.innerHTML=e+e}var M=["Toyota","Nissan","Ford","Volkswagen","Honda","Chevrolet","Hyundai","Kia","Mazda","BMW","Mercedes-Benz","Audi","Renault","Jeep","Dodge","Ram","Subaru","Mitsubishi","Suzuki","Peugeot","Volvo","Fiat","Chrysler","Acura","Infiniti","Lexus","Lincoln","Buick","GMC","Cadillac","Porsche","Mini","Seat","Alfa Romeo","Land Rover","Jaguar"];fetch("/api/catalog/brands").then((function(t){return t.json()})).then((function(t){t&&t.length>0?b(t):b(M.map((function(t,e){return{id_brand:e,name_brand:t}})))})).catch((function(){b(M.map((function(t,e){return{id_brand:e,name_brand:t}})))})),fetch("/api/catalog/stats").then((function(t){return t.json()})).then((function(t){[{format:"1.5M",key:"parts"},{format:"304K",key:"aftermarket_parts"},{format:"15.8M",key:"cross_references"},{format:"num",key:"brands"}].forEach((function(e){var n=document.querySelector('[data-format="'+e.format+'"]'),a=t[e.key];n&&"number"==typeof a&&a>0&&(n.setAttribute("data-target",a),n._animated&&(-1!==e.format.indexOf("M")?n.textContent=(a/1e6).toFixed(1)+"M+":-1!==e.format.indexOf("K")?n.textContent=Math.floor(a/1e3)+"K+":n.textContent=String(a)))}))})).catch((function(){})),document.querySelectorAll('.header-nav a[href^="#"]').forEach((function(t){t.addEventListener("click",(function(e){e.preventDefault();var n=document.querySelector(t.getAttribute("href"));n&&n.scrollIntoView({behavior:"smooth",block:"start"})}))}));var p=document.querySelector(".site-header");p&&window.addEventListener("scroll",(function(){window.scrollY>80?p.style.background="var(--glass-bg-strong)":p.style.background="var(--glass-bg)"}),{passive:!0})}(); \ No newline at end of file diff --git a/dashboard/login.min.css b/dashboard/login.min.css new file mode 100644 index 0000000..17443d9 --- /dev/null +++ b/dashboard/login.min.css @@ -0,0 +1,211 @@ +/* ============================================================ + login.css -- Login / Register page styles + ============================================================ */ + +.login-page { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + background: var(--bg-primary); + padding: 2rem; +} + +/* --- Card --- */ +.login-card { + width: 100%; + max-width: 440px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 16px; + padding: 2.5rem; + animation: fadeIn 0.4s ease; +} + +/* --- Brand header --- */ +.login-brand { + text-align: center; + margin-bottom: 2rem; +} + +.login-brand .logo-icon { + width: 56px; + height: 56px; + background: linear-gradient(135deg, var(--accent) 0%, #ff4500 100%); + border-radius: 14px; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 1.6rem; + margin-bottom: 1rem; + box-shadow: 0 4px 20px var(--accent-glow); +} + +.login-brand h1 { + font-family: 'Orbitron', sans-serif; + font-size: 1.5rem; + font-weight: 700; + letter-spacing: 2px; + color: var(--text-primary); + margin-bottom: 0.4rem; +} + +.login-brand h1 span { + color: var(--accent); +} + +.login-brand .slogan { + font-size: 0.85rem; + color: var(--text-secondary); + font-weight: 400; +} + +/* --- Form panel visibility --- */ +.form-panel { + display: none; +} + +.form-panel.active { + display: block; + animation: fadeIn 0.3s ease; +} + +/* --- Form title --- */ +.form-title { + font-size: 1.15rem; + font-weight: 600; + margin-bottom: 1.5rem; + text-align: center; + color: var(--text-primary); +} + +/* --- Select (dropdown) --- */ +.form-select { + width: 100%; + padding: 0.75rem 1rem; + background: var(--bg-tertiary); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text-primary); + font-size: 0.95rem; + transition: border-color 0.2s; + appearance: none; + -webkit-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23a0a0b0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 1rem center; + cursor: pointer; +} + +.form-select:focus { + outline: none; + border-color: var(--accent); +} + +.form-select option { + background: var(--bg-secondary); + color: var(--text-primary); +} + +/* --- Submit button (full width) --- */ +.btn-submit { + width: 100%; + padding: 0.85rem; + margin-top: 0.5rem; + font-size: 1rem; +} + +/* --- Toggle link --- */ +.toggle-link { + text-align: center; + margin-top: 1.5rem; + font-size: 0.9rem; + color: var(--text-secondary); +} + +.toggle-link a { + color: var(--accent); + text-decoration: none; + font-weight: 600; + cursor: pointer; + transition: color 0.2s; +} + +.toggle-link a:hover { + color: var(--accent-hover); + text-decoration: underline; +} + +/* --- Alert messages --- */ +.login-alert { + padding: 0.85rem 1rem; + border-radius: 8px; + margin-bottom: 1.25rem; + font-size: 0.9rem; + display: none; + align-items: center; + gap: 0.5rem; + line-height: 1.4; +} + +.login-alert.show { + display: flex; +} + +.login-alert.error { + background: rgba(255, 68, 68, 0.1); + border: 1px solid var(--danger); + color: var(--danger); +} + +.login-alert.success { + background: rgba(0, 214, 143, 0.1); + border: 1px solid var(--success); + color: var(--success); +} + +/* --- Loading spinner on button --- */ +.btn-submit.loading { + pointer-events: none; + opacity: 0.7; +} + +.btn-submit .spinner { + display: none; + width: 18px; + height: 18px; + border: 2px solid rgba(255,255,255,0.3); + border-top-color: #fff; + border-radius: 50%; + animation: spin 0.6s linear infinite; +} + +.btn-submit.loading .spinner { + display: inline-block; +} + +.btn-submit.loading .btn-label { + display: none; +} + +/* --- Row layout for two fields side by side --- */ +.form-row { + display: flex; + gap: 1rem; +} + +.form-row .form-group { + flex: 1; +} + +/* --- Responsive --- */ +@media (max-width: 500px) { + .login-card { + padding: 1.75rem 1.5rem; + } + + .form-row { + flex-direction: column; + gap: 0; + } +} diff --git a/dashboard/login.min.js b/dashboard/login.min.js new file mode 100644 index 0000000..397a360 --- /dev/null +++ b/dashboard/login.min.js @@ -0,0 +1 @@ +!function(){"use strict";const e=document.getElementById("loginPanel"),t=document.getElementById("registerPanel"),o=document.getElementById("loginForm"),n=document.getElementById("registerForm"),r=document.getElementById("alert"),a={ADMIN:"/demo",OWNER:"/demo",BODEGA:"/bodega",TALLER:"/demo"};function s(e,t){r.textContent=e,r.className="login-alert show "+t}function c(){r.className="login-alert",r.textContent=""}function l(e,t){e.classList.toggle("loading",t)}!function(){const e=localStorage.getItem("access_token"),t=localStorage.getItem("user_role");if(e&&t){const e=a[t]||"/index.html";window.location.replace(e)}}(),window.showPanel=function(o){c(),"register"===o?(e.classList.remove("active"),t.classList.add("active")):(t.classList.remove("active"),e.classList.add("active"))},o.addEventListener("submit",(async function(e){e.preventDefault(),c();const t=document.getElementById("loginEmail").value.trim(),n=document.getElementById("loginPassword").value,r=o.querySelector(".btn-submit");if(t&&n){l(r,!0);try{const e=await fetch("/api/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:t,password:n})}),o=await e.json();if(!e.ok)return void s(o.error||o.message||"Credenciales incorrectas.","error");localStorage.setItem("access_token",o.access_token),localStorage.setItem("refresh_token",o.refresh_token||""),localStorage.setItem("user_role",o.role||o.user?.role||""),localStorage.setItem("user_name",o.name||o.user?.name||"");const r=(o.role||o.user?.role||"").toUpperCase(),c=a[r]||"/index.html";window.location.replace(c)}catch(e){s("Error de conexion. Intenta de nuevo.","error")}finally{l(r,!1)}}else s("Completa todos los campos.","error")})),n.addEventListener("submit",(async function(e){e.preventDefault(),c();const t=document.getElementById("regName").value.trim(),o=document.getElementById("regEmail").value.trim(),r=document.getElementById("regPassword").value,a=document.getElementById("regConfirm").value,i=document.getElementById("regBusiness").value.trim(),m=document.getElementById("regPhone").value.trim(),d=document.getElementById("regRole").value,u=n.querySelector(".btn-submit");if(t&&o&&r&&a&&i&&m)if(r.length<8)s("La contrasena debe tener al menos 8 caracteres.","error");else if(r===a){l(u,!0);try{const e=await fetch("/api/auth/register",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:t,email:o,password:r,role:d,business_name:i,phone:m})}),a=await e.json();if(!e.ok)return void s(a.error||a.message||"Error al crear la cuenta.","error");s("Cuenta creada. Pendiente de aprobacion por administrador.","success"),n.reset()}catch(e){s("Error de conexion. Intenta de nuevo.","error")}finally{l(u,!1)}}else s("Las contrasenas no coinciden.","error");else s("Completa todos los campos.","error")})),window.authFetch=async function(e,t={}){const o=localStorage.getItem("access_token");if(!o)return void window.location.replace("/login.html");const n=Object.assign({},t.headers||{},{Authorization:"Bearer "+o});let r=await fetch(e,Object.assign({},t,{headers:n}));if(401===r.status){const o=await async function(){const e=localStorage.getItem("refresh_token");if(!e)return!1;try{const t=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refresh_token:e})});if(!t.ok)return!1;const o=await t.json();return localStorage.setItem("access_token",o.access_token),o.refresh_token&&localStorage.setItem("refresh_token",o.refresh_token),!0}catch(e){return!1}}();if(!o)return void logout();n.Authorization="Bearer "+localStorage.getItem("access_token"),r=await fetch(e,Object.assign({},t,{headers:n}))}return r},window.logout=function(){localStorage.removeItem("access_token"),localStorage.removeItem("refresh_token"),localStorage.removeItem("user_role"),localStorage.removeItem("user_name"),window.location.replace("/login.html")}}(); \ No newline at end of file diff --git a/dashboard/nav.min.js b/dashboard/nav.min.js new file mode 100644 index 0000000..c357958 --- /dev/null +++ b/dashboard/nav.min.js @@ -0,0 +1 @@ +!function(){"use strict";var e=window.location.pathname;function t(t){var a=t.replace(/\/+$/,"")||"/",n=e.replace(/\/+$/,"")||"/";return a===n||(!("/"!==a&&"/index.html"!==a||"/"!==n&&"/index.html"!==n)||(!("/admin.html"!==a&&"/admin"!==a||"/admin.html"!==n&&"/admin"!==n)||(!("/diagramas"!==a&&"/diagrams.html"!==a||"/diagramas"!==n&&"/diagrams.html"!==n)||("/customer-landing.html"===a&&"/customer-landing.html"===n||("/captura"===a&&"/captura"===n||("/pos"===a&&"/pos"===n||("/cuentas"===a&&"/cuentas"===n||("/tienda"===a&&"/tienda"===n||("/bodega"===a&&"/bodega"===n||!("/demo"!==a&&"/demo.html"!==a||"/demo"!==n&&"/demo.html"!==n))))))))))}var a='
⚙️
NEXUS AUTOPARTS
',n=document.getElementById("shared-nav");n&&(n.innerHTML=a);var r=localStorage.getItem("access_token");if(r)try{var i=JSON.parse(atob(r.split(".")[1])),o=document.getElementById("nav-user-name"),l=document.getElementById("nav-auth-btn");o&&i.business_name?o.textContent=i.business_name:o&&(o.textContent=i.role||""),l&&(l.textContent="Salir",l.href="#",l.onclick=function(e){e.preventDefault(),localStorage.removeItem("access_token"),localStorage.removeItem("refresh_token"),window.location.href="/login.html"})}catch(e){}}(); \ No newline at end of file diff --git a/dashboard/pos.min.css b/dashboard/pos.min.css new file mode 100644 index 0000000..99280cb --- /dev/null +++ b/dashboard/pos.min.css @@ -0,0 +1,418 @@ +/* ============================================================ + pos.css -- Point of Sale styles + ============================================================ */ + +.pos-container { + max-width: 1400px; + margin: 0 auto; + padding: 5rem 2rem 2rem; +} + +/* --- Layout: 2 columns --- */ +.pos-layout { + display: grid; + grid-template-columns: 1fr 360px; + gap: 1.5rem; + align-items: start; +} + +/* --- Left: Search + Cart --- */ +.pos-main { + display: flex; + flex-direction: column; + gap: 1rem; +} + +/* --- Customer bar --- */ +.customer-bar { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + padding: 1rem; + display: flex; + gap: 1rem; + align-items: center; +} + +.customer-bar .cb-search { + flex: 1; + padding: 0.5rem 0.8rem; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text-primary); + font-size: 0.9rem; +} + +.customer-bar .cb-search:focus { + outline: none; + border-color: var(--accent); +} + +.customer-bar .cb-selected { + display: flex; + align-items: center; + gap: 0.8rem; + flex: 1; +} + +.customer-bar .cb-name { + font-weight: 700; + font-size: 1rem; +} + +.customer-bar .cb-rfc { + font-size: 0.8rem; + color: var(--text-secondary); + font-family: monospace; +} + +.customer-bar .cb-balance { + font-size: 0.85rem; + padding: 0.2rem 0.6rem; + border-radius: 6px; +} + +.cb-balance.positive { background: rgba(255, 68, 68, 0.15); color: var(--danger); } +.cb-balance.zero { background: rgba(34, 197, 94, 0.15); color: var(--success); } + +/* --- Customer dropdown --- */ +.customer-dropdown { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 0 0 8px 8px; + max-height: 250px; + overflow-y: auto; + z-index: 100; + box-shadow: 0 8px 30px rgba(0,0,0,0.4); +} + +.customer-dropdown-item { + padding: 0.6rem 1rem; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid var(--border); +} + +.customer-dropdown-item:hover { + background: var(--bg-hover); +} + +.customer-dropdown-item .cdi-name { font-weight: 600; } +.customer-dropdown-item .cdi-rfc { font-size: 0.8rem; color: var(--text-secondary); } + +/* --- Part search --- */ +.part-search-wrap { + position: relative; +} + +.part-search { + width: 100%; + padding: 0.7rem 1rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + color: var(--text-primary); + font-size: 1rem; +} + +.part-search:focus { + outline: none; + border-color: var(--accent); +} + +.part-results { + position: absolute; + top: 100%; + left: 0; + right: 0; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 0 0 10px 10px; + max-height: 300px; + overflow-y: auto; + z-index: 100; + box-shadow: 0 8px 30px rgba(0,0,0,0.4); + display: none; +} + +.part-result-item { + padding: 0.6rem 1rem; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid var(--border); +} + +.part-result-item:hover, +.part-result-item.part-result-active { + background: var(--bg-hover); + border-left: 3px solid var(--accent); +} + +.part-result-item .pri-number { + font-family: monospace; + font-weight: 600; + color: var(--accent); +} + +.part-result-item .pri-name { + font-size: 0.85rem; + color: var(--text-secondary); + margin-left: 0.5rem; +} + +.part-result-item .pri-type { + font-size: 0.7rem; + padding: 0.15rem 0.4rem; + border-radius: 4px; + text-transform: uppercase; +} + +.pri-type.oem { background: rgba(59, 130, 246, 0.15); color: var(--info); } +.pri-type.aftermarket { background: rgba(245, 158, 11, 0.15); color: var(--warning); } + +/* --- Cart table --- */ +.cart-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + overflow: hidden; +} + +.cart-card h3 { + padding: 0.8rem 1rem; + border-bottom: 1px solid var(--border); + font-size: 0.9rem; + color: var(--text-secondary); +} + +.cart-table { + width: 100%; + border-collapse: collapse; + font-size: 0.85rem; +} + +.cart-table th { + text-align: left; + padding: 0.5rem 0.6rem; + border-bottom: 1px solid var(--border); + color: var(--text-secondary); + font-size: 0.75rem; + text-transform: uppercase; +} + +.cart-table td { + padding: 0.5rem 0.6rem; + border-bottom: 1px solid rgba(42, 42, 58, 0.5); + vertical-align: middle; +} + +.cart-table input { + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 4px; + color: var(--text-primary); + padding: 0.3rem 0.4rem; + font-size: 0.85rem; + width: 70px; + text-align: right; +} + +.cart-table input:focus { + outline: none; + border-color: var(--accent); +} + +.cart-table .cart-desc { max-width: 250px; } +.cart-table .cart-qty { width: 45px; text-align: center; } +.cart-table .cart-cost { width: 80px; } +.cart-table .cart-margin { width: 55px; } +.cart-table .cart-price { width: 80px; } + +.cart-table .cart-remove { + background: none; + border: none; + color: var(--danger); + cursor: pointer; + font-size: 1rem; + padding: 0.2rem; +} + +.cart-empty { + text-align: center; + padding: 2rem; + color: var(--text-secondary); + font-size: 0.9rem; +} + +/* --- Right sidebar: Invoice summary --- */ +.pos-sidebar { + position: sticky; + top: 5rem; +} + +.invoice-summary { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + padding: 1.2rem; +} + +.invoice-summary h3 { + font-size: 0.9rem; + color: var(--text-secondary); + margin-bottom: 1rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.summary-row { + display: flex; + justify-content: space-between; + padding: 0.4rem 0; + font-size: 0.9rem; +} + +.summary-row.total { + border-top: 2px solid var(--accent); + margin-top: 0.5rem; + padding-top: 0.8rem; + font-size: 1.2rem; + font-weight: 700; +} + +.summary-row .sr-label { color: var(--text-secondary); } +.summary-row .sr-value { font-weight: 600; } +.summary-row.total .sr-value { color: var(--accent); } + +.btn-facturar { + width: 100%; + margin-top: 1.2rem; + padding: 0.9rem; + font-size: 1rem; + background: linear-gradient(135deg, var(--accent), #ff4500); + border: none; + border-radius: 10px; + color: #fff; + font-weight: 700; + cursor: pointer; + transition: all 0.3s; +} + +.btn-facturar:hover { + transform: translateY(-2px); + box-shadow: 0 6px 25px var(--accent-glow); +} + +.btn-facturar:disabled { + opacity: 0.4; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +.invoice-notes { + width: 100%; + margin-top: 0.8rem; + padding: 0.5rem; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-primary); + font-size: 0.85rem; + resize: vertical; + min-height: 60px; +} + +.invoice-notes:focus { + outline: none; + border-color: var(--accent); +} + +/* --- New customer modal --- */ +.modal-overlay { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(0,0,0,0.6); + z-index: 2000; + display: flex; + align-items: center; + justify-content: center; +} + +.modal-content { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 12px; + padding: 1.5rem; + width: 450px; + max-width: 95vw; +} + +.modal-content h3 { + margin-bottom: 1rem; + font-size: 1.1rem; +} + +.modal-field { + margin-bottom: 0.8rem; +} + +.modal-field label { + display: block; + font-size: 0.75rem; + color: var(--text-secondary); + text-transform: uppercase; + margin-bottom: 0.2rem; +} + +.modal-field input { + width: 100%; + padding: 0.5rem; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 6px; + color: var(--text-primary); + font-size: 0.9rem; +} + +.modal-field input:focus { + outline: none; + border-color: var(--accent); +} + +.modal-actions { + display: flex; + gap: 0.5rem; + justify-content: flex-end; + margin-top: 1rem; +} + +/* --- Toast (reuse from captura) --- */ +.toast { + position: fixed; + bottom: 2rem; + right: 2rem; + padding: 0.8rem 1.5rem; + border-radius: 10px; + color: #fff; + font-weight: 600; + font-size: 0.9rem; + z-index: 9999; + animation: toastIn 0.3s ease; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); +} +.toast.success { background: var(--success); } +.toast.error { background: var(--danger); } +@keyframes toastIn { + from { transform: translateY(20px); opacity: 0; } + to { transform: translateY(0); opacity: 1; } +} diff --git a/dashboard/pos.min.js b/dashboard/pos.min.js new file mode 100644 index 0000000..d572375 --- /dev/null +++ b/dashboard/pos.min.js @@ -0,0 +1 @@ +!function(){"use strict";var e=null,t=[];function n(e,t){var n=document.createElement("div");n.className="toast "+(t||"success"),n.textContent=e,document.body.appendChild(n),setTimeout((function(){n.remove()}),3e3)}function a(e,t){return t=t||{},fetch(""+e,t).then((function(e){return e.ok?e.json():e.json().then((function(e){throw new Error(e.error||"Error")}))}))}function r(e){if(!e)return"";var t=document.createElement("div");return t.textContent=e,t.innerHTML}function i(e){return"$"+(parseFloat(e)||0).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g,",")}var c=null,o=document.getElementById("customer-search"),s=document.getElementById("customer-dropdown");function d(t){a("/api/pos/customers/"+t).then((function(t){e=t,document.getElementById("customer-select").style.display="none",document.getElementById("customer-info").style.display="flex",document.getElementById("sel-customer-name").textContent=t.name,document.getElementById("sel-customer-rfc").textContent=t.rfc||"Sin RFC";var n=document.getElementById("sel-customer-balance");n.textContent="Saldo: "+i(t.balance),n.className="cb-balance "+(t.balance>0?"positive":"zero"),s.style.display="none",_()}))}o.addEventListener("input",(function(){clearTimeout(c);var e=this.value.trim();e.length<2?s.style.display="none":c=setTimeout((function(){a("/api/pos/customers?search="+encodeURIComponent(e)+"&per_page=10").then((function(e){var t=e.data||[];0===t.length?s.innerHTML='
No se encontraron clientes
':(s.innerHTML=t.map((function(e){return'
'+r(e.name)+""+(e.rfc?' '+r(e.rfc)+"":"")+'
'+i(e.balance)+"
"})).join(""),s.querySelectorAll(".customer-dropdown-item").forEach((function(e){e.addEventListener("click",(function(){d(parseInt(e.getAttribute("data-id")))}))}))),s.style.display="block"}))}),300)})),o.addEventListener("blur",(function(){setTimeout((function(){s.style.display="none"}),200)})),document.getElementById("btn-change-customer").addEventListener("click",(function(){e=null,document.getElementById("customer-info").style.display="none",document.getElementById("customer-select").style.display="block",o.value="",o.focus(),_()})),document.getElementById("btn-new-customer").addEventListener("click",(function(){document.getElementById("modal-new-customer").style.display="flex",document.getElementById("nc-name").focus()})),document.getElementById("nc-cancel").addEventListener("click",(function(){document.getElementById("modal-new-customer").style.display="none"})),document.getElementById("nc-save").addEventListener("click",(function(){var e=document.getElementById("nc-name").value.trim();e?a("/api/pos/customers",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,rfc:document.getElementById("nc-rfc").value.trim()||null,business_name:document.getElementById("nc-business").value.trim()||null,phone:document.getElementById("nc-phone").value.trim()||null,email:document.getElementById("nc-email").value.trim()||null,address:document.getElementById("nc-address").value.trim()||null,credit_limit:parseFloat(document.getElementById("nc-credit").value)||0,payment_terms:parseInt(document.getElementById("nc-terms").value)||30})}).then((function(t){n("Cliente creado: "+e),document.getElementById("modal-new-customer").style.display="none",d(t.id),["nc-name","nc-rfc","nc-business","nc-phone","nc-email","nc-address"].forEach((function(e){document.getElementById(e).value=""})),document.getElementById("nc-credit").value="0",document.getElementById("nc-terms").value="30"})).catch((function(e){n(e.message,"error")})):n("Ingresa el nombre del cliente","error")}));var u=null,l=document.getElementById("part-search"),m=document.getElementById("part-results"),p=[],y=-1;function f(){m.querySelectorAll(".part-result-item").forEach((function(e,t){t===y?(e.classList.add("part-result-active"),e.scrollIntoView({block:"nearest"})):e.classList.remove("part-result-active")}))}function v(e){var n;e>=0&&e'+r(e.description)+''+e.part_type+'%'+i(e.unit_price)+""+i(n)+''})).join(""),e.querySelectorAll("input").forEach((function(e){e.addEventListener("change",(function(){var n=parseInt(e.getAttribute("data-idx")),a=e.getAttribute("data-field"),r=parseFloat(e.value)||0;t[n][a]=r,"unit_cost"!==a&&"margin_pct"!==a||(t[n].unit_price=t[n].unit_cost*(1+t[n].margin_pct/100)),g()}))})),e.querySelectorAll(".cart-remove").forEach((function(e){e.addEventListener("click",(function(){t.splice(parseInt(e.getAttribute("data-idx")),1),g()}))})),E()}function E(){var e=t.reduce((function(e,t){return e+t.quantity}),0),n=t.reduce((function(e,t){return e+t.quantity*t.unit_price}),0),a=.16*n,r=n+a;document.getElementById("sum-items").textContent=e,document.getElementById("sum-subtotal").textContent=i(n),document.getElementById("sum-tax").textContent=i(a),document.getElementById("sum-total").textContent=i(r),_()}function _(){document.getElementById("btn-facturar").disabled=!(e&&t.length>0)}l.addEventListener("input",(function(){var e=l.value.trim();if(e.length<1)return m.style.display="none",void(p=[]);clearTimeout(u),u=setTimeout((function(){a("/api/pos/search-parts?q="+encodeURIComponent(e)).then((function(e){p=e,y=-1,function(){if(0===p.length&&l.value.trim().length>0)return m.innerHTML='
No se encontraron partes para "'+r(l.value)+'"
',void(m.style.display="block");if(0===p.length)return void(m.style.display="none");m.innerHTML=p.map((function(e,t){return'
'+r(e.oem_part_number)+''+r(e.name_part)+'
'+e.part_type+""+(e.cost_usd?''+i(e.cost_usd)+"":"")+''+r(e.group_name||"")+"
"})).join(""),m.querySelectorAll(".part-result-item").forEach((function(e){e.addEventListener("mousedown",(function(t){t.preventDefault(),v(parseInt(e.getAttribute("data-idx")))})),e.addEventListener("mouseenter",(function(){y=parseInt(e.getAttribute("data-idx")),f()}))})),m.style.display="block"}()}))}),150)})),l.addEventListener("keydown",(function(e){"none"!==m.style.display&&0!==p.length&&("ArrowDown"===e.key?(e.preventDefault(),y=Math.min(y+1,p.length-1),f()):"ArrowUp"===e.key?(e.preventDefault(),y=Math.max(y-1,0),f()):"Enter"===e.key?(e.preventDefault(),y>=0?v(y):1===p.length&&v(0)):"Escape"===e.key&&(m.style.display="none",y=-1))})),l.addEventListener("focus",(function(){p.length>0&&(m.style.display="block")})),l.addEventListener("blur",(function(){setTimeout((function(){m.style.display="none"}),200)})),document.getElementById("btn-facturar").addEventListener("click",(function(){if(e&&0!==t.length){var r=this;r.disabled=!0,r.textContent="Generando...";var c=t.map((function(e){return{part_id:e.part_id,aftermarket_id:e.aftermarket_id,description:e.description,quantity:e.quantity,unit_cost:e.unit_cost,margin_pct:e.margin_pct,unit_price:Math.round(100*e.unit_price)/100}}));a("/api/pos/invoices",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({customer_id:e.id_customer,items:c,notes:document.getElementById("invoice-notes").value.trim()})}).then((function(a){n("Factura "+a.folio+" creada por "+i(a.total)),t=[],g(),document.getElementById("invoice-notes").value="",d(e.id_customer),r.textContent="Facturar"})).catch((function(e){n(e.message,"error"),r.disabled=!1,r.textContent="Facturar"}))}})),g()}(); \ No newline at end of file diff --git a/dashboard/shared.min.css b/dashboard/shared.min.css new file mode 100644 index 0000000..3868194 --- /dev/null +++ b/dashboard/shared.min.css @@ -0,0 +1,262 @@ +/* ============================================================ + shared.css -- Common styles for all Nexus Autoparts pages + ============================================================ */ + +/* --- Reset --- */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +/* --- CSS Variables (union of all pages) --- */ +:root { + --bg-primary: #0a0a0f; + --bg-secondary: #12121a; + --bg-card: #1a1a24; + --bg-hover: #252532; + --bg-tertiary: #1a1a25; + --accent: #ff6b35; + --accent-hover: #ff8555; + --accent-glow: rgba(255, 107, 53, 0.3); + --text-primary: #ffffff; + --text-secondary: #a0a0b0; + --border: #2a2a3a; + --success: #22c55e; + --warning: #f59e0b; + --info: #3b82f6; + --danger: #ff4444; +} + +/* --- Base body --- */ +body { + font-family: 'Inter', sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + min-height: 100vh; +} + +/* --- Shared Button Styles --- */ +.btn { + padding: 0.7rem 1.5rem; + border-radius: 10px; + border: none; + font-weight: 600; + cursor: pointer; + transition: all 0.3s; + font-size: 0.9rem; + text-decoration: none; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +.btn-primary { + background: linear-gradient(135deg, var(--accent) 0%, #ff4500 100%); + color: white; + box-shadow: 0 4px 15px var(--accent-glow); +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 6px 25px var(--accent-glow); +} + +.btn-secondary { + background: var(--bg-card); + border: 1px solid var(--border); + color: var(--text-primary); +} + +.btn-secondary:hover { + border-color: var(--accent); + color: var(--accent); +} + +.btn-back { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.6rem 1.2rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + color: var(--text-primary); + font-weight: 500; + cursor: pointer; + transition: all 0.3s; + margin-bottom: 1.5rem; +} + +.btn-back:hover { + border-color: var(--accent); + color: var(--accent); +} + +/* --- Shared Animations --- */ +@keyframes spin { + to { transform: rotate(360deg); } +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +/* --- Loading & Empty States --- */ +.state-container { + text-align: center; + padding: 4rem 2rem; + color: var(--text-secondary); +} + +.state-container i { + font-size: 4rem; + margin-bottom: 1rem; + color: var(--text-secondary); +} + +.state-container h4 { + color: var(--text-primary); + margin-bottom: 0.5rem; +} + +/* --- Scrollbar Styling --- */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: var(--bg-secondary); +} + +::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--accent); +} + +/* --- Skip Link (accessibility) --- */ +.skip-link { + position: absolute; + top: -50px; + left: 0; + background: var(--accent); + color: white; + padding: 0.75rem 1.5rem; + z-index: 3000; + text-decoration: none; + font-weight: 600; + border-radius: 0 0 8px 0; +} + +.skip-link:focus { + top: 0; +} + +/* --- Screen Reader Only --- */ +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* --- Alert / Toast Styles --- */ +.alert { + padding: 1rem 1.5rem; + border-radius: 8px; + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.alert-success { + background: rgba(0, 214, 143, 0.1); + border: 1px solid var(--success); + color: var(--success); +} + +.alert-error { + background: rgba(255, 68, 68, 0.1); + border: 1px solid var(--danger); + color: var(--danger); +} + +/* --- Modal Base Styles --- */ +.modal-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.8); + z-index: 2000; + align-items: center; + justify-content: center; + padding: 2rem; +} + +.modal-overlay.active { + display: flex; +} + +/* --- Form Styles --- */ +.form-group { + margin-bottom: 1.25rem; +} + +.form-label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + font-size: 0.9rem; + color: var(--text-secondary); +} + +.form-input { + width: 100%; + padding: 0.75rem 1rem; + background: var(--bg-tertiary); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text-primary); + font-size: 0.95rem; + transition: border-color 0.2s; +} + +.form-input:focus { + outline: none; + border-color: var(--accent); +} + +.form-input::placeholder { + color: var(--text-secondary); +} + +/* --- Quality Badges --- */ +.quality-badge { + display: inline-block; + padding: 0.25rem 0.6rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.quality-economy { background: var(--warning); color: #000; } +.quality-standard { background: var(--info); color: white; } +.quality-premium { background: var(--success); color: white; } +.quality-oem { background: #9b59b6; color: white; } diff --git a/dashboard/tienda.min.css b/dashboard/tienda.min.css new file mode 100644 index 0000000..403dd99 --- /dev/null +++ b/dashboard/tienda.min.css @@ -0,0 +1,678 @@ +/* ============================================================ + tienda.css -- Store / Tablet dashboard styles + Nexus Autoparts — tablet-first, touch-friendly + ============================================================ */ + +/* --- Base overrides for tienda page --- */ +body { + margin: 0; + padding: 0; + font-family: 'DM Sans', sans-serif; + background: var(--bg-primary); + color: var(--text-primary); + min-height: 100vh; + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + overscroll-behavior: none; +} + +/* --- Header --- */ +.t-header { + position: fixed; + top: 0; left: 0; right: 0; + z-index: 100; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.6rem 1.2rem; + background: rgba(18, 18, 26, 0.92); + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); + border-bottom: 1px solid var(--border); +} + +.t-header-left { + display: flex; + align-items: center; + gap: 0.6rem; + flex-shrink: 0; +} + +.t-logo-mark { + width: 36px; + height: 36px; + background: linear-gradient(135deg, var(--accent) 0%, #ff4500 100%); + border-radius: 9px; + box-shadow: 0 3px 14px var(--accent-glow); + display: flex; + align-items: center; + justify-content: center; +} + +.t-logo-mark::after { + content: '\2699\FE0F'; + font-size: 1.2rem; +} + +.t-brand { + display: flex; + flex-direction: column; + line-height: 1.1; +} + +.t-brand-name { + font-family: 'Outfit', sans-serif; + font-weight: 800; + font-size: 1.1rem; + background: linear-gradient(135deg, #fff 0%, var(--accent) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.t-brand-sub { + font-size: 0.55rem; + font-weight: 600; + color: var(--text-secondary); + letter-spacing: 0.15em; + text-transform: uppercase; +} + +/* --- Header center: search --- */ +.t-header-center { + flex: 1; + max-width: 420px; + margin: 0 1rem; +} + +.t-search-box { + position: relative; + display: flex; + align-items: center; +} + +.t-search-icon { + position: absolute; + left: 0.7rem; + width: 18px; + height: 18px; + color: var(--text-secondary); + pointer-events: none; +} + +.t-search-box input { + width: 100%; + padding: 0.55rem 0.8rem 0.55rem 2.2rem; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + color: var(--text-primary); + font-family: 'DM Sans', sans-serif; + font-size: 0.85rem; + outline: none; + transition: border-color 0.2s; +} + +.t-search-box input:focus { + border-color: var(--accent); +} + +.t-search-box input::placeholder { + color: var(--text-secondary); +} + +.t-search-results { + position: absolute; + top: calc(100% + 4px); + left: 0; right: 0; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 10px; + max-height: 300px; + overflow-y: auto; + box-shadow: 0 12px 40px rgba(0,0,0,0.5); + display: none; + z-index: 200; +} + +.t-search-results.active { + display: block; +} + +.t-search-result-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.6rem 0.8rem; + border-bottom: 1px solid var(--border); + cursor: pointer; + transition: background 0.15s; +} + +.t-search-result-item:last-child { + border-bottom: none; +} + +.t-search-result-item:hover, +.t-search-result-item:active { + background: var(--bg-hover); +} + +.t-search-result-item .sri-number { + font-family: 'JetBrains Mono', monospace; + font-weight: 600; + font-size: 0.85rem; + color: var(--accent); +} + +.t-search-result-item .sri-name { + font-size: 0.8rem; + color: var(--text-secondary); + margin-left: 0.4rem; +} + +/* --- Header right: clock --- */ +.t-header-right { + flex-shrink: 0; +} + +.t-clock { + font-family: 'JetBrains Mono', monospace; + font-size: 0.85rem; + font-weight: 500; + color: var(--text-secondary); + letter-spacing: 0.03em; +} + +/* --- Main --- */ +.t-main { + padding: 4.2rem 1rem 1.5rem; + max-width: 1200px; + margin: 0 auto; +} + +/* --- KPI Row --- */ +.t-kpi-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 0.8rem; + margin-bottom: 1rem; +} + +.t-kpi { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 12px; + padding: 0.9rem 1rem; + display: flex; + align-items: center; + gap: 0.8rem; + position: relative; + overflow: hidden; + transition: transform 0.2s, box-shadow 0.2s; +} + +.t-kpi:active { + transform: scale(0.98); +} + +/* Colored left accent bar */ +.t-kpi::before { + content: ''; + position: absolute; + left: 0; top: 0; bottom: 0; + width: 3px; + border-radius: 3px 0 0 3px; +} + +.t-kpi[data-color="accent"]::before { background: var(--accent); } +.t-kpi[data-color="success"]::before { background: var(--success); } +.t-kpi[data-color="info"]::before { background: var(--info); } +.t-kpi[data-color="warning"]::before { background: var(--warning); } + +.t-kpi-icon { + width: 40px; + height: 40px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.t-kpi-icon svg { + width: 22px; + height: 22px; +} + +.t-kpi[data-color="accent"] .t-kpi-icon { background: rgba(255, 107, 53, 0.12); color: var(--accent); } +.t-kpi[data-color="success"] .t-kpi-icon { background: rgba(34, 197, 94, 0.12); color: var(--success); } +.t-kpi[data-color="info"] .t-kpi-icon { background: rgba(59, 130, 246, 0.12); color: var(--info); } +.t-kpi[data-color="warning"] .t-kpi-icon { background: rgba(245, 158, 11, 0.12); color: var(--warning); } + +.t-kpi-data { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; +} + +.t-kpi-value { + font-family: 'Outfit', sans-serif; + font-weight: 700; + font-size: 1.3rem; + line-height: 1.2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.t-kpi-label { + font-size: 0.72rem; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.04em; + font-weight: 500; +} + +.t-kpi-count { + font-size: 0.65rem; + color: var(--text-secondary); + font-family: 'JetBrains Mono', monospace; + white-space: nowrap; + align-self: flex-start; + margin-top: 0.2rem; +} + +/* --- Content Grid --- */ +.t-content { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.8rem; +} + +/* --- Cards --- */ +.t-card { + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 12px; + padding: 1rem; +} + +.t-card-full { + min-height: 0; +} + +.t-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.8rem; +} + +.t-card-title { + font-family: 'DM Sans', sans-serif; + font-size: 0.85rem; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.04em; + margin-bottom: 0.8rem; +} + +.t-card-header .t-card-title { + margin-bottom: 0; +} + +.t-see-all { + font-size: 0.75rem; + color: var(--accent); + text-decoration: none; + font-weight: 600; + padding: 0.3rem 0.6rem; + border-radius: 6px; + transition: background 0.2s; +} + +.t-see-all:hover, +.t-see-all:active { + background: rgba(255, 107, 53, 0.1); +} + +/* --- Quick Actions Grid --- */ +.t-actions-card { + padding-bottom: 0.8rem; +} + +.t-actions-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.6rem; +} + +.t-action { + display: flex; + align-items: center; + gap: 0.7rem; + padding: 0.8rem; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 10px; + text-decoration: none; + color: var(--text-primary); + font-size: 0.85rem; + font-weight: 600; + transition: transform 0.15s, background 0.2s, border-color 0.2s; + -webkit-tap-highlight-color: transparent; +} + +.t-action:active { + transform: scale(0.96); +} + +.t-action:hover { + background: var(--bg-hover); +} + +.t-action-icon { + width: 36px; + height: 36px; + border-radius: 9px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.t-action-icon svg { + width: 20px; + height: 20px; +} + +.t-action[data-color="accent"] .t-action-icon { background: rgba(255, 107, 53, 0.12); color: var(--accent); } +.t-action[data-color="accent"]:hover { border-color: var(--accent); } +.t-action[data-color="info"] .t-action-icon { background: rgba(59, 130, 246, 0.12); color: var(--info); } +.t-action[data-color="info"]:hover { border-color: var(--info); } +.t-action[data-color="success"] .t-action-icon { background: rgba(34, 197, 94, 0.12); color: var(--success); } +.t-action[data-color="success"]:hover { border-color: var(--success); } +.t-action[data-color="warning"] .t-action-icon { background: rgba(245, 158, 11, 0.12); color: var(--warning); } +.t-action[data-color="warning"]:hover { border-color: var(--warning); } + +/* --- Debtors List --- */ +.t-debtors-list { + display: flex; + flex-direction: column; + gap: 0.4rem; + max-height: 280px; + overflow-y: auto; +} + +.t-debtor { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.6rem 0.7rem; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 8px; + cursor: pointer; + transition: background 0.15s, border-color 0.15s; +} + +.t-debtor:hover, +.t-debtor:active { + background: var(--bg-hover); + border-color: var(--danger); +} + +.t-debtor-name { + font-weight: 600; + font-size: 0.85rem; +} + +.t-debtor-invoices { + font-size: 0.7rem; + color: var(--text-secondary); +} + +.t-debtor-amount { + font-family: 'JetBrains Mono', monospace; + font-weight: 700; + font-size: 0.9rem; + color: var(--danger); +} + +/* --- Invoice List --- */ +.t-invoice-list { + display: flex; + flex-direction: column; + gap: 0.4rem; + max-height: 320px; + overflow-y: auto; +} + +.t-invoice { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.6rem 0.7rem; + background: var(--bg-primary); + border: 1px solid var(--border); + border-radius: 8px; + transition: background 0.15s; +} + +.t-invoice:hover, +.t-invoice:active { + background: var(--bg-hover); +} + +.t-invoice-left { + display: flex; + flex-direction: column; + gap: 0.15rem; +} + +.t-invoice-folio { + font-family: 'JetBrains Mono', monospace; + font-weight: 700; + font-size: 0.85rem; + color: var(--accent); +} + +.t-invoice-customer { + font-size: 0.75rem; + color: var(--text-secondary); +} + +.t-invoice-right { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.15rem; +} + +.t-invoice-total { + font-family: 'JetBrains Mono', monospace; + font-weight: 600; + font-size: 0.85rem; +} + +.t-invoice-status { + font-size: 0.65rem; + font-weight: 600; + padding: 0.15rem 0.45rem; + border-radius: 4px; + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.t-invoice-status.paid { + background: rgba(34, 197, 94, 0.15); + color: var(--success); +} + +.t-invoice-status.pending { + background: rgba(245, 158, 11, 0.15); + color: var(--warning); +} + +.t-invoice-status.partial { + background: rgba(59, 130, 246, 0.15); + color: var(--info); +} + +.t-invoice-status.cancelled { + background: rgba(255, 68, 68, 0.15); + color: var(--danger); +} + +/* --- Today's Payments card --- */ +.t-today-payments { + text-align: center; + padding: 0.5rem 0; +} + +.t-today-amount { + font-family: 'Outfit', sans-serif; + font-weight: 800; + font-size: 2rem; + color: var(--success); + line-height: 1.2; +} + +.t-today-count { + font-size: 0.8rem; + color: var(--text-secondary); + margin-top: 0.3rem; +} + +/* --- Empty state --- */ +.t-empty { + text-align: center; + padding: 1.5rem; + color: var(--text-secondary); + font-size: 0.85rem; +} + +/* --- Scrollbar (minimal for touch) --- */ +.t-debtors-list::-webkit-scrollbar, +.t-invoice-list::-webkit-scrollbar, +.t-search-results::-webkit-scrollbar { + width: 4px; +} + +.t-debtors-list::-webkit-scrollbar-track, +.t-invoice-list::-webkit-scrollbar-track, +.t-search-results::-webkit-scrollbar-track { + background: transparent; +} + +.t-debtors-list::-webkit-scrollbar-thumb, +.t-invoice-list::-webkit-scrollbar-thumb, +.t-search-results::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 2px; +} + +/* --- Responsive --- */ + +/* Tablet landscape (default target) */ +@media (max-width: 1024px) { + .t-main { + padding: 4rem 0.8rem 1.2rem; + } + + .t-kpi-row { + grid-template-columns: repeat(2, 1fr); + } +} + +/* Tablet portrait / large phone */ +@media (max-width: 768px) { + .t-header-center { + display: none; + } + + .t-main { + padding: 3.8rem 0.6rem 1rem; + } + + .t-content { + grid-template-columns: 1fr; + } + + .t-kpi-row { + grid-template-columns: repeat(2, 1fr); + gap: 0.6rem; + } + + .t-kpi { + padding: 0.7rem 0.8rem; + } + + .t-kpi-value { + font-size: 1.1rem; + } + + .t-kpi-count { + display: none; + } + + .t-actions-grid { + grid-template-columns: 1fr 1fr; + } +} + +/* Small phone */ +@media (max-width: 480px) { + .t-kpi-row { + grid-template-columns: 1fr 1fr; + } + + .t-kpi-icon { + width: 32px; + height: 32px; + } + + .t-kpi-icon svg { + width: 18px; + height: 18px; + } + + .t-kpi-value { + font-size: 1rem; + } + + .t-actions-grid { + grid-template-columns: 1fr; + } +} + +/* --- Fade-in animation for cards --- */ +@keyframes t-fadeIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +.t-kpi { + animation: t-fadeIn 0.4s ease both; +} + +.t-kpi:nth-child(1) { animation-delay: 0.05s; } +.t-kpi:nth-child(2) { animation-delay: 0.1s; } +.t-kpi:nth-child(3) { animation-delay: 0.15s; } +.t-kpi:nth-child(4) { animation-delay: 0.2s; } + +.t-card { + animation: t-fadeIn 0.4s ease both; + animation-delay: 0.25s; +} + +.t-content .t-col:nth-child(2) .t-card { + animation-delay: 0.3s; +} + +.t-content .t-col:nth-child(2) .t-card:nth-child(2) { + animation-delay: 0.35s; +} diff --git a/dashboard/tienda.min.js b/dashboard/tienda.min.js new file mode 100644 index 0000000..1cc6452 --- /dev/null +++ b/dashboard/tienda.min.js @@ -0,0 +1 @@ +!function(){"use strict";function t(t){return"$"+(parseFloat(t)||0).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g,",")}function e(t){if(!t)return"";var e=document.createElement("div");return e.textContent=t,e.innerHTML}function n(){var t=new Date,e=t.getHours(),n=String(t.getMinutes()).padStart(2,"0"),i=e>=12?"PM":"AM";e=e%12||12,document.getElementById("clock").textContent=e+":"+n+" "+i}function i(){fetch("/api/tienda/stats").then((function(t){return t.json()})).then((function(n){var i=n.sales_today||{},a=n.sales_month||{},o=n.payments_today||{};document.getElementById("kpi-sales-today").textContent=t(i.total),document.getElementById("kpi-sales-count").textContent=(i.count||0)+" facturas",document.getElementById("kpi-month").textContent=t(a.total),document.getElementById("kpi-month-count").textContent=(a.count||0)+" facturas",document.getElementById("kpi-customers").textContent=n.total_customers||0,document.getElementById("kpi-parts-count").textContent=(n.total_parts||0)+" partes",document.getElementById("kpi-pending").textContent=t(n.pending_balance||0),document.getElementById("kpi-pending-count").textContent=(n.pending_invoices||0)+" facturas",document.getElementById("payments-today-amount").textContent=t(o.total),document.getElementById("payments-today-count").textContent=(o.count||0)+" pagos registrados",function(n){var i=document.getElementById("debtors-list");if(0===n.length)return void(i.innerHTML='
Sin cuentas pendientes
');i.innerHTML=n.map((function(n){var i=n.credit_limit>0?Math.round(n.balance/n.credit_limit*100):0;return'
'+e(n.name)+"
"+(n.credit_limit>0?'
'+i+"% de límite
":"")+'
'+t(n.balance)+"
"})).join("")}(n.top_debtors||[]),function(n){var i=document.getElementById("recent-invoices");if(0===n.length)return void(i.innerHTML='
Sin facturas recientes
');i.innerHTML=n.map((function(n){var i=n.status||"pending",a={pending:"Pendiente",paid:"Pagada",partial:"Parcial",cancelled:"Cancelada"};return'
'+e(n.folio)+''+e(n.customer_name)+'
'+t(n.total)+''+(a[i]||i)+"
"})).join("")}(n.recent_invoices||[])})).catch((function(t){console.error("Error loading stats:",t)}))}n(),setInterval(n,3e4);var a=null,o=document.getElementById("global-search"),s=document.getElementById("global-results");o&&(o.addEventListener("input",(function(){clearTimeout(a);var t=this.value.trim();if(t.length<2)return s.classList.remove("active"),void(s.innerHTML="");a=setTimeout((function(){fetch("/api/pos/search-parts?q="+encodeURIComponent(t)).then((function(t){return t.json()})).then((function(n){0===n.length?s.innerHTML='
Sin resultados para "'+e(t)+'"
':s.innerHTML=n.slice(0,8).map((function(t){return'
'+e(t.oem_part_number)+''+e(t.name_part)+"
"})).join(""),s.classList.add("active")}))}),250)})),o.addEventListener("blur",(function(){setTimeout((function(){s.classList.remove("active")}),200)})),o.addEventListener("focus",(function(){s.innerHTML.trim()&&s.classList.add("active")}))),i(),setInterval(i,12e4)}(); \ No newline at end of file diff --git a/nginx/nexus-pos.conf b/nginx/nexus-pos.conf index 3f47ceb..32b98ab 100644 --- a/nginx/nexus-pos.conf +++ b/nginx/nexus-pos.conf @@ -32,6 +32,21 @@ server { add_header X-Content-Type-Options nosniff always; } + # Auto-serve minified JS/CSS when available (transparent to templates) + location ~* ^(.+)\.js$ { + try_files $1.min.js $uri =404; + expires 6M; + add_header Cache-Control "public, immutable"; + add_header X-Content-Type-Options nosniff always; + } + + location ~* ^(.+)\.css$ { + try_files $1.min.css $uri =404; + expires 6M; + add_header Cache-Control "public, immutable"; + add_header X-Content-Type-Options nosniff always; + } + location / { proxy_pass http://nexus_main; proxy_set_header Host $host; diff --git a/pos/static/css/chat.min.css b/pos/static/css/chat.min.css new file mode 100644 index 0000000..ec6a47e --- /dev/null +++ b/pos/static/css/chat.min.css @@ -0,0 +1,385 @@ +/* ========================================================================== + NEXUS POS — AI Chat Widget + Uses design system tokens from tokens.css + ========================================================================== */ + +/* ─── Floating Button ─── */ + +.chat-fab { + position: fixed; + bottom: 140px; /* above cart FAB and F-keys footer */ + right: var(--space-5); + z-index: 8000; + width: 52px; + height: 52px; + border-radius: var(--radius-full); + border: none; + cursor: pointer; + background: var(--color-accent); + color: #fff; + font-size: 1.5rem; + display: flex; + align-items: center; + justify-content: center; + box-shadow: var(--shadow-lg); + transition: transform var(--duration-fast) var(--ease-in-out), + background var(--duration-fast) var(--ease-in-out); +} + +.chat-fab:hover { + transform: scale(1.08); + background: var(--color-primary-hover, #e5952f); +} + +.chat-fab.has-unread::after { + content: ''; + position: absolute; + top: 4px; + right: 4px; + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--color-error); +} + +/* ─── Chat Panel ─── */ + +.chat-panel { + position: fixed; + bottom: 200px; + right: var(--space-5); + z-index: 8001; + width: 400px; + height: 520px; + max-height: calc(100vh - 100px); + display: flex; + flex-direction: column; + background: var(--color-bg-elevated); + border: 1px solid var(--color-border); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-xl); + overflow: hidden; + transform: translateY(20px) scale(0.95); + opacity: 0; + pointer-events: none; + transition: transform var(--duration-normal) var(--ease-in-out), + opacity var(--duration-normal) var(--ease-in-out); +} + +.chat-panel.open { + transform: translateY(0) scale(1); + opacity: 1; + pointer-events: all; +} + +/* ─── Header ─── */ + +.chat-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-3) var(--space-4); + background: var(--color-accent); + color: #fff; + flex-shrink: 0; +} + +.chat-header h3 { + font-family: var(--font-heading); + font-size: var(--text-body); + font-weight: var(--font-weight-semibold); + margin: 0; +} + +.chat-header-close { + background: none; + border: none; + color: #fff; + font-size: 1.2rem; + cursor: pointer; + padding: var(--space-1); + line-height: 1; + opacity: 0.8; +} + +.chat-header-close:hover { opacity: 1; } + +/* ─── Messages Area ─── */ + +.chat-messages { + flex: 1; + overflow-y: auto; + padding: var(--space-3); + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +/* ─── Message Bubbles ─── */ + +.chat-msg { + max-width: 85%; + padding: var(--space-2) var(--space-3); + border-radius: var(--radius-lg); + font-size: var(--text-body-sm, 0.875rem); + line-height: 1.45; + word-wrap: break-word; +} + +.chat-msg.user { + align-self: flex-end; + background: var(--color-accent); + color: #fff; + border-bottom-right-radius: var(--radius-sm, 4px); +} + +.chat-msg.ai { + align-self: flex-start; + background: var(--color-surface-2, rgba(255,255,255,0.06)); + color: var(--color-text-primary); + border-bottom-left-radius: var(--radius-sm, 4px); +} + +/* ─── Typing Indicator ─── */ + +.chat-typing { + align-self: flex-start; + display: none; + gap: 4px; + padding: var(--space-2) var(--space-3); + background: var(--color-surface-2, rgba(255,255,255,0.06)); + border-radius: var(--radius-lg); + border-bottom-left-radius: var(--radius-sm, 4px); +} + +.chat-typing.visible { display: flex; } + +.chat-typing span { + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--color-text-muted); + animation: chatBounce 1.2s infinite; +} +.chat-typing span:nth-child(2) { animation-delay: 0.2s; } +.chat-typing span:nth-child(3) { animation-delay: 0.4s; } + +@keyframes chatBounce { + 0%, 60%, 100% { transform: translateY(0); } + 30% { transform: translateY(-5px); } +} + +/* ─── Part Result Cards ─── */ + +.chat-parts { + display: flex; + flex-direction: column; + gap: var(--space-2); + margin-top: var(--space-2); +} + +.chat-part-card { + background: var(--color-bg-elevated); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: var(--space-2) var(--space-3); + cursor: pointer; + transition: border-color var(--duration-fast) var(--ease-in-out), + background var(--duration-fast) var(--ease-in-out); +} + +.chat-part-card:hover { + border-color: var(--color-accent); + background: var(--color-bg-base); +} + +.chat-part-card .part-number { + font-family: var(--font-mono); + font-size: var(--text-caption, 0.75rem); + color: var(--color-accent); + font-weight: var(--font-weight-semibold); +} + +.chat-part-card .part-name { + font-size: var(--text-body-sm, 0.875rem); + color: var(--color-text-primary); + margin-top: 2px; +} + +.chat-part-card .part-stock { + font-size: var(--text-caption, 0.75rem); + color: var(--color-text-muted); + margin-top: 2px; +} + +.chat-part-card .part-stock.in-stock { + color: var(--color-success); +} + +/* ─── Input Area ─── */ + +.chat-input-area { + display: flex; + gap: var(--space-2); + padding: var(--space-3); + border-top: 1px solid var(--color-border); + flex-shrink: 0; +} + +.chat-input { + flex: 1; + padding: var(--space-2) var(--space-3); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + background: var(--color-bg-base); + color: var(--color-text-primary); + font-size: var(--text-body-sm, 0.875rem); + font-family: var(--font-body); + resize: none; + outline: none; + min-height: 38px; + max-height: 80px; +} + +.chat-input:focus { + border-color: var(--color-accent); +} + +.chat-input::placeholder { + color: var(--color-text-muted); +} + +.chat-send-btn { + width: 38px; + height: 38px; + border-radius: var(--radius-md); + border: none; + background: var(--color-accent); + color: #fff; + font-size: 1.1rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: background var(--duration-fast) var(--ease-in-out); +} + +.chat-send-btn:hover { background: var(--color-primary-hover, #e5952f); } +.chat-send-btn:disabled { opacity: 0.5; cursor: not-allowed; } + +/* ─── Camera Button (Photo identification) ─── */ + +.chat-cam-btn { + width: 38px; + height: 38px; + border-radius: var(--radius-md); + border: 1px solid var(--color-border); + background: var(--color-bg-base); + color: var(--color-text-secondary); + font-size: 1.1rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: background var(--duration-fast) var(--ease-in-out), + color var(--duration-fast) var(--ease-in-out), + border-color var(--duration-fast) var(--ease-in-out); +} + +.chat-cam-btn:hover { + border-color: var(--color-accent); + color: var(--color-accent); +} + +.chat-msg-image img { + border: 1px solid var(--color-border); +} + +/* ─── Mic Button (Voice Input) ─── */ + +.chat-mic-btn { + width: 38px; + height: 38px; + border-radius: var(--radius-md); + border: 1px solid var(--color-border); + background: var(--color-bg-base); + color: var(--color-text-secondary); + font-size: 1.1rem; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: background var(--duration-fast) var(--ease-in-out), + color var(--duration-fast) var(--ease-in-out), + border-color var(--duration-fast) var(--ease-in-out); +} + +.chat-mic-btn:hover { + border-color: var(--color-accent); + color: var(--color-accent); +} + +.chat-mic-btn.listening { + background: #f85149; + border-color: #f85149; + color: #fff; + animation: micPulse 1.4s infinite; +} + +@keyframes micPulse { + 0%, 100% { box-shadow: 0 0 0 0 rgba(248, 81, 73, 0.4); } + 50% { box-shadow: 0 0 0 10px rgba(248, 81, 73, 0); } +} + +/* ─── Voice Toast ─── */ + +.chat-voice-toast { + position: fixed; + bottom: 160px; + left: 50%; + transform: translateX(-50%) translateY(10px); + background: rgba(0, 0, 0, 0.8); + color: #fff; + padding: 8px 18px; + border-radius: var(--radius-md, 8px); + font-size: 0.85rem; + z-index: 9999; + opacity: 0; + transition: opacity 0.3s, transform 0.3s; + pointer-events: none; +} + +.chat-voice-toast.visible { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + +/* ─── Vehicle Info Banner ─── */ + +.chat-vehicle-banner { + margin-top: var(--space-2); + padding: var(--space-2) var(--space-3); + background: var(--color-bg-base); + border: 1px solid var(--color-accent); + border-left: 3px solid var(--color-accent); + border-radius: var(--radius-md); + font-size: var(--text-caption, 0.75rem); + color: var(--color-text-secondary); +} + +.chat-vehicle-banner strong { + color: var(--color-text-primary); +} + +/* ─── Responsive ─── */ + +@media (max-width: 480px) { + .chat-panel { + width: calc(100vw - var(--space-4)); + right: var(--space-2); + height: 60vh; + } +} diff --git a/pos/static/css/common.min.css b/pos/static/css/common.min.css new file mode 100644 index 0000000..e79fc00 --- /dev/null +++ b/pos/static/css/common.min.css @@ -0,0 +1,85 @@ +/* /home/Autopartes/pos/static/css/common.css */ +/* Theme variables — overridden by tenant theme */ +:root { + --color-primary: #1a73e8; + --color-secondary: #5f6368; + --color-accent: #ff6b35; + --color-bg: #ffffff; + --color-surface: #f8f9fa; + --color-text: #202124; + --color-text-secondary: #5f6368; + --color-border: #dadce0; + --color-success: #34a853; + --color-warning: #f9ab00; + --color-error: #ea4335; + --font-display: 'Sora', sans-serif; + --font-body: 'Plus Jakarta Sans', sans-serif; + --font-mono: 'JetBrains Mono', monospace; + --radius: 8px; + --shadow: 0 1px 3px rgba(0,0,0,0.12); +} + +*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; } + +body { + font-family: var(--font-body); + background: var(--color-bg); + color: var(--color-text); + line-height: 1.6; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 10px 20px; + border: 1px solid var(--color-border); + border-radius: var(--radius); + font-family: var(--font-body); + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + background: var(--color-surface); + color: var(--color-text); +} + +.btn:hover { background: var(--color-border); } +.btn--primary { background: var(--color-primary); color: white; border-color: var(--color-primary); } +.btn--primary:hover { opacity: 0.9; } +.btn--accent { background: var(--color-accent); color: white; border-color: var(--color-accent); } + +.card { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius); + padding: 24px; +} + +/* Catalog grid */ +.catalog-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 16px; } +.catalog-card { cursor: pointer; transition: all 0.2s; } +.catalog-card:hover { border-color: var(--color-primary); transform: translateY(-2px); box-shadow: var(--shadow); } +.stock-badge { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 0.75rem; font-weight: 600; } +.stock-badge--ok { background: #dcfce7; color: #166534; } +.stock-badge--low { background: #fef9c3; color: #854d0e; } +.stock-badge--zero { background: #fecaca; color: #991b1b; } + +/* Cart sidebar */ +.cart-sidebar { position: fixed; right: 0; top: 0; bottom: 0; width: 360px; background: var(--color-surface); border-left: 1px solid var(--color-border); padding: 20px; overflow-y: auto; transform: translateX(100%); transition: transform 0.3s; z-index: 50; } +.cart-sidebar.open { transform: translateX(0); } +.cart-item { display: flex; gap: 12px; padding: 12px 0; border-bottom: 1px solid var(--color-border); } +.cart-total { font-family: var(--font-mono); font-size: 1.3rem; font-weight: 700; } + +/* Search bar */ +.search-bar { display: flex; gap: 8px; margin-bottom: 20px; } +.search-bar input { flex: 1; padding: 10px 16px; border: 1px solid var(--color-border); border-radius: var(--radius); font-size: 1rem; } +.search-bar input:focus { outline: none; border-color: var(--color-primary); } + +/* Filter chips */ +.filter-chips { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; } +.chip { padding: 4px 12px; border-radius: 20px; border: 1px solid var(--color-border); font-size: 0.8rem; cursor: pointer; background: transparent; } +.chip.active { background: var(--color-primary); color: white; border-color: var(--color-primary); } + +/* External availability */ +.external-results { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: var(--radius); padding: 16px; margin-top: 12px; } diff --git a/pos/static/css/onboarding.min.css b/pos/static/css/onboarding.min.css new file mode 100644 index 0000000..94165ad --- /dev/null +++ b/pos/static/css/onboarding.min.css @@ -0,0 +1,428 @@ +/* ========================================================================== + NEXUS POS — Onboarding Wizard + Uses design system tokens (works with both industrial + modern themes) + ========================================================================== */ + +/* Overlay backdrop */ +.onboarding-overlay { + position: fixed; + inset: 0; + z-index: var(--z-modal); + display: flex; + align-items: center; + justify-content: center; + background: var(--overlay-backdrop); + opacity: 0; + animation: onb-fade-in var(--duration-normal) var(--ease-out) forwards; +} + +@keyframes onb-fade-in { + to { opacity: 1; } +} + +/* Modal card */ +.onboarding-modal { + width: 92vw; + max-width: 500px; + max-height: 90vh; + background: var(--color-bg-elevated); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-xl); + display: flex; + flex-direction: column; + overflow: hidden; + transform: translateY(20px); + animation: onb-slide-up var(--duration-normal) var(--ease-out) forwards; +} + +[data-theme="industrial"] .onboarding-modal { + border-radius: 0; + clip-path: polygon(0 0, calc(100% - 20px) 0, 100% 20px, 100% 100%, 0 100%); +} + +[data-theme="modern"] .onboarding-modal { + border-radius: var(--radius-lg); +} + +@keyframes onb-slide-up { + to { transform: translateY(0); } +} + +/* Step content area */ +.onboarding-body { + padding: var(--space-8) var(--space-6); + overflow-y: auto; + flex: 1; +} + +/* Step icon */ +.onb-icon { + width: 64px; + height: 64px; + margin: 0 auto var(--space-5); + display: flex; + align-items: center; + justify-content: center; + font-size: 2rem; + background: var(--color-primary-muted); + color: var(--color-primary); +} + +[data-theme="industrial"] .onb-icon { + border-radius: 0; + clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%); +} + +[data-theme="modern"] .onb-icon { + border-radius: var(--radius-lg); +} + +/* Titles */ +.onb-title { + font-family: var(--font-heading); + font-weight: var(--heading-weight-primary); + font-size: var(--text-h4); + color: var(--color-text-primary); + text-align: center; + margin-bottom: var(--space-2); + letter-spacing: var(--tracking-snug); +} + +[data-theme="industrial"] .onb-title { + text-transform: uppercase; +} + +.onb-desc { + font-size: var(--text-body-sm); + color: var(--color-text-secondary); + text-align: center; + line-height: var(--leading-body-sm); + margin-bottom: var(--space-6); +} + +/* Form fields inside wizard */ +.onb-form { + display: flex; + flex-direction: column; + gap: var(--space-3); +} + +.onb-field { + display: flex; + flex-direction: column; + gap: var(--space-1); +} + +.onb-label { + font-size: var(--text-label); + font-weight: var(--font-weight-semibold); + color: var(--color-text-secondary); + letter-spacing: var(--tracking-wide); +} + +[data-theme="industrial"] .onb-label { + text-transform: uppercase; + font-size: var(--text-caption); + letter-spacing: var(--tracking-wider); +} + +.onb-input { + height: 40px; + padding: 0 var(--space-3); + background: var(--color-bg-base); + border: 1px solid var(--color-border); + color: var(--color-text-primary); + font-family: var(--font-body); + font-size: var(--text-body-sm); + transition: var(--transition-fast); + outline: none; +} + +[data-theme="industrial"] .onb-input { + border-radius: 0; +} + +[data-theme="modern"] .onb-input { + border-radius: var(--radius-md); +} + +.onb-input:focus { + border-color: var(--color-border-focus); + box-shadow: var(--shadow-focus); +} + +.onb-input::placeholder { + color: var(--color-text-muted); +} + +.onb-select { + height: 40px; + padding: 0 var(--space-3); + background: var(--color-bg-base); + border: 1px solid var(--color-border); + color: var(--color-text-primary); + font-family: var(--font-body); + font-size: var(--text-body-sm); + transition: var(--transition-fast); + outline: none; + cursor: pointer; +} + +[data-theme="industrial"] .onb-select { + border-radius: 0; +} + +[data-theme="modern"] .onb-select { + border-radius: var(--radius-md); +} + +.onb-select:focus { + border-color: var(--color-border-focus); + box-shadow: var(--shadow-focus); +} + +/* Inline error */ +.onb-error { + font-size: var(--text-caption); + color: var(--color-error); + min-height: 18px; + margin-top: var(--space-1); +} + +/* Success message in step */ +.onb-success { + display: flex; + align-items: center; + gap: var(--space-2); + padding: var(--space-3); + background: var(--color-success-light); + color: var(--color-success-dark); + font-size: var(--text-body-sm); + font-weight: var(--font-weight-semibold); + margin-top: var(--space-3); +} + +[data-theme="industrial"] .onb-success { + border-radius: 0; +} + +[data-theme="modern"] .onb-success { + border-radius: var(--radius-md); +} + +/* Footer: buttons + progress */ +.onboarding-footer { + padding: var(--space-4) var(--space-6); + border-top: 1px solid var(--color-border); + display: flex; + flex-direction: column; + gap: var(--space-4); + background: var(--color-bg-elevated); +} + +.onb-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); +} + +/* Buttons */ +.onb-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + padding: 0 var(--space-5); + height: 40px; + font-family: var(--font-body); + font-size: var(--text-body-sm); + font-weight: var(--font-weight-semibold); + border: 1px solid transparent; + cursor: pointer; + transition: var(--transition-fast); + white-space: nowrap; + letter-spacing: var(--tracking-wide); +} + +[data-theme="industrial"] .onb-btn { + border-radius: 0; + clip-path: polygon(0 0, calc(100% - 10px) 0, 100% 10px, 100% 100%, 0 100%); + text-transform: uppercase; +} + +[data-theme="modern"] .onb-btn { + border-radius: var(--radius-md); +} + +.onb-btn--primary { + background: var(--btn-primary-bg); + color: var(--btn-primary-text); +} + +.onb-btn--primary:hover { + background: var(--btn-primary-bg-hover); +} + +.onb-btn--primary:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.onb-btn--secondary { + background: var(--btn-secondary-bg); + color: var(--btn-secondary-text); + border-color: var(--btn-secondary-border); +} + +.onb-btn--secondary:hover { + background: var(--btn-secondary-bg-hover); +} + +.onb-btn--ghost { + background: transparent; + color: var(--color-text-muted); + border-color: transparent; +} + +.onb-btn--ghost:hover { + color: var(--color-text-primary); +} + +/* Progress dots */ +.onb-progress { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-2); +} + +.onb-dot { + width: 10px; + height: 10px; + border: 2px solid var(--color-border-strong); + background: transparent; + transition: var(--transition-normal); +} + +[data-theme="industrial"] .onb-dot { + border-radius: 0; +} + +[data-theme="modern"] .onb-dot { + border-radius: var(--radius-full); +} + +.onb-dot.is-active { + background: var(--color-primary); + border-color: var(--color-primary); + transform: scale(1.2); +} + +.onb-dot.is-done { + background: var(--color-primary-muted); + border-color: var(--color-primary); +} + +/* Step counter label */ +.onb-step-label { + font-size: var(--text-caption); + color: var(--color-text-muted); + text-align: center; + letter-spacing: var(--tracking-wide); +} + +/* "No mostrar de nuevo" checkbox row */ +.onb-dismiss-row { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-2); + padding-top: var(--space-2); +} + +.onb-dismiss-row label { + font-size: var(--text-caption); + color: var(--color-text-muted); + cursor: pointer; +} + +.onb-dismiss-row input[type="checkbox"] { + accent-color: var(--color-primary); + cursor: pointer; +} + +/* Final step link grid */ +.onb-links { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: var(--space-3); + margin-top: var(--space-4); +} + +.onb-link-card { + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-2); + padding: var(--space-4) var(--space-3); + background: var(--color-bg-base); + border: 1px solid var(--color-border); + text-decoration: none; + color: var(--color-text-primary); + font-size: var(--text-body-sm); + font-weight: var(--font-weight-semibold); + transition: var(--transition-fast); +} + +[data-theme="industrial"] .onb-link-card { + border-radius: 0; +} + +[data-theme="modern"] .onb-link-card { + border-radius: var(--radius-md); +} + +.onb-link-card:hover { + border-color: var(--color-primary); + color: var(--color-primary); + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.onb-link-card span.onb-link-icon { + font-size: 1.5rem; +} + +/* Step transition */ +.onb-step-enter { + animation: onb-step-in var(--duration-normal) var(--ease-out) forwards; +} + +@keyframes onb-step-in { + from { + opacity: 0; + transform: translateX(20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +/* Responsive */ +@media (max-width: 480px) { + .onboarding-modal { + width: 96vw; + max-height: 95vh; + } + .onboarding-body { + padding: var(--space-6) var(--space-4); + } + .onboarding-footer { + padding: var(--space-3) var(--space-4); + } + .onb-links { + grid-template-columns: 1fr; + } +} diff --git a/pos/static/css/pos-glass.min.css b/pos/static/css/pos-glass.min.css new file mode 100644 index 0000000..fc7943f --- /dev/null +++ b/pos/static/css/pos-glass.min.css @@ -0,0 +1,683 @@ +/* ========================================================================== + POS-GLASS.CSS — Pixel-Perfect glassmorphism overlay for Nexus POS + Load AFTER tokens.css. Applies glass effects, glow, 3D buttons, + and animations to all POS pages without modifying inline styles. + ========================================================================== */ + +/* ── Hidden scrollbar (global) ── */ +html { scrollbar-width: none; } +html::-webkit-scrollbar { width: 0; } + +/* ── Smooth font rendering ── */ +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* ========================================================================== + SIDEBAR — Glass treatment + ========================================================================== */ + +.sidebar, +.pos-sidebar { + background: var(--glass-bg-strong) !important; + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-right: 1px solid var(--glass-border) !important; +} + +.sidebar__logo { + position: relative; +} + +.sidebar__logo-text { + position: relative; +} + +/* Glow under logo text */ +.sidebar__logo-text::after { + content: ''; + position: absolute; + bottom: -4px; + left: 0; + right: 0; + height: 2px; + background: var(--gradient-accent); + border-radius: 1px; + opacity: 0.4; + filter: blur(2px); +} + +/* Nav items — hover glow */ +.sidebar__nav a, +.sidebar__nav-item, +.sidebar .nav-item { + transition: all 0.25s var(--ease-out) !important; + border-radius: var(--radius-md); +} + +.sidebar__nav a:hover, +.sidebar__nav-item:hover, +.sidebar .nav-item:hover { + box-shadow: 0 0 12px var(--glow-color-soft); +} + +.sidebar__nav a.active, +.sidebar__nav-item.active, +.sidebar .nav-item.active { + box-shadow: 0 0 16px var(--glow-color-soft), inset 0 0 0 1px var(--glass-border); +} + +/* ========================================================================== + THEME BAR — Glass + ========================================================================== */ + +.theme-bar { + background: var(--glass-bg-strong) !important; + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border-bottom: 1px solid var(--glass-border) !important; +} + +/* ========================================================================== + CARDS — Glass with glow hover + ========================================================================== */ + +.kpi-card, +.table-card, +.card, +.stat-card, +.chart-card, +.alert-card, +.config-card, +.fleet-card, +.report-card, +.invoice-card, +.customer-card, +.panel { + background: var(--glass-bg) !important; + backdrop-filter: blur(var(--glass-blur)); + -webkit-backdrop-filter: blur(var(--glass-blur)); + border: 1px solid var(--glass-border) !important; + transition: all 0.3s var(--ease-out) !important; + position: relative; + overflow: hidden; +} + +/* Accent top-line on hover */ +.kpi-card::before, +.table-card::before, +.chart-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: var(--gradient-accent); + transform: scaleX(0); + transform-origin: left; + transition: transform 0.4s var(--ease-out); + z-index: 1; +} + +.kpi-card:hover::before, +.table-card:hover::before, +.chart-card:hover::before { + transform: scaleX(1); +} + +.kpi-card:hover, +.table-card:hover, +.card:hover, +.stat-card:hover, +.chart-card:hover, +.config-card:hover, +.fleet-card:hover, +.report-card:hover { + border-color: var(--color-border-accent) !important; + box-shadow: 0 4px 20px var(--glow-color-soft); +} + +/* KPI card accent bar — add glow */ +.kpi-card__accent-bar { + box-shadow: 0 0 8px var(--glow-color-soft); +} + +/* ========================================================================== + BUTTONS — 3D depth effect + ========================================================================== */ + +/* Primary buttons */ +.btn--primary, +button.primary, +.btn-primary, +input[type="submit"], +button[type="submit"] { + background: var(--gradient-accent) !important; + border: none !important; + box-shadow: 0 3px 0 var(--color-primary-active), + 0 4px 10px var(--glow-color-soft) !important; + transition: all 0.25s var(--ease-out) !important; + position: relative; + overflow: hidden; +} + +.btn--primary:hover, +button.primary:hover, +.btn-primary:hover, +input[type="submit"]:hover, +button[type="submit"]:hover { + transform: translateY(-1px); + box-shadow: 0 4px 0 var(--color-primary-active), + 0 8px 20px var(--glow-color) !important; +} + +.btn--primary:active, +button.primary:active, +.btn-primary:active, +input[type="submit"]:active, +button[type="submit"]:active { + transform: translateY(1px); + box-shadow: 0 1px 0 var(--color-primary-active) !important; +} + +/* Ghost / secondary buttons — glass */ +.btn--ghost, +.btn--secondary, +.btn-secondary, +.btn-ghost, +button.secondary { + background: var(--glass-bg) !important; + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + border: 1px solid var(--glass-border) !important; + transition: all 0.25s var(--ease-out) !important; +} + +.btn--ghost:hover, +.btn--secondary:hover, +.btn-secondary:hover, +.btn-ghost:hover, +button.secondary:hover { + border-color: var(--color-border-accent) !important; + box-shadow: 0 0 16px var(--glow-color-soft); +} + +/* ========================================================================== + INPUTS — Glass with focus glow + ========================================================================== */ + +input[type="text"], +input[type="number"], +input[type="email"], +input[type="password"], +input[type="search"], +input[type="tel"], +input[type="date"], +input[type="url"], +textarea, +select, +.search-input, +.filter-input { + background: var(--glass-bg) !important; + border: 1px solid var(--glass-border) !important; + transition: all 0.25s var(--ease-out) !important; +} + +input[type="text"]:focus, +input[type="number"]:focus, +input[type="email"]:focus, +input[type="password"]:focus, +input[type="search"]:focus, +input[type="tel"]:focus, +input[type="date"]:focus, +input[type="url"]:focus, +textarea:focus, +select:focus, +.search-input:focus, +.filter-input:focus { + border-color: var(--color-border-focus) !important; + box-shadow: 0 0 0 3px var(--glow-color-soft), 0 0 16px var(--glow-color-soft) !important; + outline: none; +} + +/* ========================================================================== + TABLES — Subtle glass rows + ========================================================================== */ + +table thead th { + background: var(--glass-bg) !important; + backdrop-filter: blur(8px); + font-family: var(--font-mono); + font-size: var(--text-caption); + text-transform: uppercase; + letter-spacing: var(--tracking-wider); +} + +table tbody tr { + transition: all 0.2s ease !important; +} + +table tbody tr:hover { + background: var(--glass-highlight) !important; + box-shadow: inset 0 0 0 1px var(--glass-border); +} + +/* ========================================================================== + MODALS — Glass overlay + glass content + ========================================================================== */ + +.modal-overlay, +.overlay, +.modal-backdrop { + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); +} + +.modal, +.modal-content, +.modal-dialog, +.dialog { + background: var(--glass-bg-strong) !important; + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); + border: 1px solid var(--glass-border) !important; + box-shadow: 0 24px 48px rgba(0,0,0,0.3) !important; +} + +/* ========================================================================== + TABS — Glass active state + ========================================================================== */ + +.tab, +.tab-btn, +.tabs button { + transition: all 0.25s var(--ease-out) !important; + border-radius: var(--radius-md); +} + +.tab.active, +.tab-btn.active, +.tabs button.active { + background: var(--color-primary-muted) !important; + box-shadow: 0 0 12px var(--glow-color-soft); + border-color: var(--color-border-accent) !important; +} + +/* ========================================================================== + BADGES / TAGS — Subtle glow + ========================================================================== */ + +.badge, +.tag, +.status-badge, +.pill { + backdrop-filter: blur(4px); + transition: all 0.2s ease; +} + +/* ========================================================================== + SCROLL REVEAL — Available for any POS page that wants it + ========================================================================== */ + +.nx-reveal { + opacity: 0; + transform: translateY(24px); + transition: opacity 0.6s var(--ease-out), transform 0.6s var(--ease-out); +} +.nx-reveal.is-visible { + opacity: 1; + transform: translateY(0); +} +.nx-stagger > .nx-reveal:nth-child(1) { transition-delay: 0ms; } +.nx-stagger > .nx-reveal:nth-child(2) { transition-delay: 80ms; } +.nx-stagger > .nx-reveal:nth-child(3) { transition-delay: 160ms; } +.nx-stagger > .nx-reveal:nth-child(4) { transition-delay: 240ms; } +.nx-stagger > .nx-reveal:nth-child(5) { transition-delay: 320ms; } +.nx-stagger > .nx-reveal:nth-child(6) { transition-delay: 400ms; } + +/* ========================================================================== + TOAST / NOTIFICATIONS — Glass + ========================================================================== */ + +.toast, +.notification, +.snackbar, +.alert { + background: var(--glass-bg-strong) !important; + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid var(--glass-border) !important; +} + +/* ========================================================================== + DROPDOWN / POPOVER — Glass + ========================================================================== */ + +.dropdown-menu, +.popover, +.autocomplete-list, +.suggestion-list { + background: var(--glass-bg-strong) !important; + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid var(--glass-border) !important; + box-shadow: 0 8px 32px rgba(0,0,0,0.2) !important; +} + +/* ========================================================================== + STATUS BAR (POS) — Glass + ========================================================================== */ + +.status-bar, +.pos-status-bar { + background: var(--glass-bg-strong) !important; + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border-bottom: 1px solid var(--glass-border) !important; +} + +/* ========================================================================== + LOADING SPINNER — Glow animation + ========================================================================== */ + +.spinner, +.loading-spinner { + animation: nx-glow-pulse 1.5s ease-in-out infinite; +} + +/* ========================================================================== + ANIMATIONS — Available keyframes + ========================================================================== */ + +@keyframes pos-fade-in { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } +} + +/* Apply subtle entry animation to main content area */ +.content, +.main-content, +main { + animation: pos-fade-in 0.4s var(--ease-out) both; +} + +/* ========================================================================== + DASHED BORDER ACCENTS (Pixel-Perfect style) + ========================================================================== */ + +.section-divider, +hr { + border: none; + border-top: 1px dashed var(--glass-border); + margin: var(--space-4) 0; +} + +/* ========================================================================== + TABLET RESPONSIVE — Adaptive layout for 768px-1024px screens + Applied globally to all POS pages via pos-glass.css. + Targets iPad (768×1024), Android tablets (800×1280), and similar. + ========================================================================== */ + +/* ── Tablet portrait (768-1023px) — sidebar collapses, grids reflow ── */ +@media (max-width: 1023px) { + + /* Sidebar collapses to an overlay drawer */ + .sidebar, + .pos-sidebar { + position: fixed !important; + top: 0 !important; + left: 0 !important; + bottom: 0 !important; + z-index: var(--z-modal) !important; + transform: translateX(-100%) !important; + transition: transform 0.3s var(--ease-out) !important; + width: 260px !important; + } + + .sidebar.open, + .pos-sidebar.open { + transform: translateX(0) !important; + box-shadow: 0 0 40px rgba(0,0,0,0.3) !important; + } + + .sidebar-overlay { + display: none !important; + position: fixed !important; + inset: 0 !important; + z-index: calc(var(--z-modal) - 1) !important; + background: rgba(0,0,0,0.5) !important; + } + + .sidebar-overlay.open { + display: block !important; + } + + /* App shell: full width when sidebar is hidden */ + .app-shell { + flex-direction: column !important; + } + + .app-shell > main, + .app-shell > .main-content, + .app-shell > .content, + .main-content, + .content { + margin-left: 0 !important; + width: 100% !important; + } + + /* Show hamburger button */ + .hamburger-btn { + display: flex !important; + } + + /* Touch-friendly targets — minimum 44px tap area */ + button, + .btn, + .nav-card, + .tab-btn, + .tab, + .part-card, + .search-result-item, + table tbody tr, + .kpi-card { + min-height: 44px; + } + + /* Larger text for readability on tablets */ + .kpi-card__value { + font-size: 1.5rem !important; + } + + /* Grid reflow: 2 columns instead of 3-4 */ + .kpi-grid { + grid-template-columns: repeat(2, 1fr) !important; + } + + .nav-grid { + grid-template-columns: repeat(2, 1fr) !important; + } + + /* Tables: horizontal scroll wrapper on narrow screens */ + .table-wrap, + .table-card { + overflow-x: auto !important; + -webkit-overflow-scrolling: touch; + } + + /* POS-specific: if the POS has a side panel (cart), stack vertically */ + .pos-layout { + flex-direction: column !important; + } + + .pos-layout .pos-cart, + .pos-layout .cart-panel { + width: 100% !important; + max-width: 100% !important; + height: auto !important; + max-height: 40vh !important; + } + + /* Content headers: tighter padding */ + .content-header, + .header, + .page-header { + padding: var(--space-3) var(--space-4) !important; + } + + /* Search bar: full width */ + .search-bar, + .search-wrapper { + width: 100% !important; + max-width: 100% !important; + } + + /* Mode toggle: slightly larger buttons for touch */ + .mode-toggle button { + padding: 6px 14px !important; + font-size: 12px !important; + } + + /* Vehicle selector dropdowns: stack on smaller tablets */ + .vehicle-selector__inner, + .vehicle-selector .vs-group { + flex-wrap: wrap !important; + } + + .vehicle-selector .vs-arrow { + display: none !important; + } + + .vehicle-selector .vs-select { + min-width: 130px !important; + } +} + +/* ── Phone portrait (< 768px) — single column, max simplification ── */ +@media (max-width: 767px) { + + .sidebar { + width: 85vw !important; + max-width: 300px !important; + } + + .kpi-grid, + .nav-grid, + .results-grid { + grid-template-columns: 1fr !important; + } + + .kpi-card__value { + font-size: 1.3rem !important; + } + + /* Stack the mode toggle buttons vertically if tight */ + .mode-toggle { + flex-wrap: wrap !important; + } + + /* Hide non-essential UI to save space */ + .header__store-badge, + .vs-vin-divider { + display: none !important; + } + + /* Full-width modals */ + .modal-content { + max-width: 95vw !important; + margin: var(--space-3) !important; + padding: var(--space-4) !important; + } + + /* Tables: force readable font size */ + table { + font-size: 12px !important; + } + + table th, + table td { + padding: var(--space-2) var(--space-2) !important; + } +} + +/* ── Landscape tablet (height < 600px with wide screen) ── */ +@media (max-height: 600px) and (min-width: 768px) { + /* Reduce vertical padding for landscape tablet use */ + .kpi-grid { + gap: var(--space-2) !important; + } + + .dashboard, + .main-content, + .content { + padding: var(--space-3) !important; + } +} + +/* ── Touch device hints ── */ +@media (hover: none) and (pointer: coarse) { + /* Remove hover-only effects on touch devices — they cause sticky states */ + .kpi-card:hover, + .nav-card:hover, + .part-card:hover, + .table-card:hover, + .card:hover { + transform: none !important; + } + + /* Larger touch targets for interactive elements */ + .sidebar__nav a, + .sidebar__nav-item, + .sidebar .nav-item { + padding: 12px 16px !important; + min-height: 48px !important; + display: flex !important; + align-items: center !important; + } + + /* Scroll momentum on iOS */ + .table-wrap, + .main-content, + .content, + .parts-grid, + .nav-grid { + -webkit-overflow-scrolling: touch; + } + + /* Disable text selection on buttons (prevents accidental blue highlight on long tap) */ + button, + .btn, + .nav-card, + .tab-btn { + -webkit-user-select: none; + user-select: none; + } +} + + +/* ========================================================================== + PRINT — Disable glass effects for printing + ========================================================================== */ + +@media print { + .sidebar, + .theme-bar, + .kpi-card, + .table-card, + .card, + .modal, + .modal-content, + table thead th, + input, + select, + textarea { + background: #fff !important; + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; + box-shadow: none !important; + border-color: #ccc !important; + color: #000 !important; + } +} diff --git a/pos/static/css/tokens.min.css b/pos/static/css/tokens.min.css new file mode 100644 index 0000000..26a4041 --- /dev/null +++ b/pos/static/css/tokens.min.css @@ -0,0 +1,627 @@ +/* ========================================================================== + NEXUS AUTOPARTS — Design Tokens + POS System for Auto Parts Stores + Version: 1.0.0 + ========================================================================== + Themes: + - [data-theme="industrial"] — Industrial Robusto (Dark) + - [data-theme="modern"] — Técnico Moderno (Light) + ========================================================================== */ + +/* -------------------------------------------------------------------------- + GOOGLE FONTS IMPORTS + -------------------------------------------------------------------------- */ + +@import url('https://fonts.googleapis.com/css2?family=Barlow:wght@400;700&family=Barlow+Condensed:wght@600;800&family=Poppins:wght@300;400;600;700&display=swap'); + + +/* ========================================================================== + GLOBAL TOKENS — Theme-independent, shared across both themes + ========================================================================== */ + +:root { + + /* ------------------------------------------------------------------------ + SEMANTIC COLORS — Status / Feedback (shared) + ------------------------------------------------------------------------ */ + + --color-success: #22c55e; + --color-success-light: #bbf7d0; + --color-success-dark: #15803d; + + --color-warning: #eab308; + --color-warning-light: #fef08a; + --color-warning-dark: #a16207; + + --color-error: #ef4444; + --color-error-light: #fecaca; + --color-error-dark: #b91c1c; + + /* ------------------------------------------------------------------------ + NEUTRAL SCALE — Grey ramp (50–900) + ------------------------------------------------------------------------ */ + + --color-neutral-50: #fafafa; + --color-neutral-100: #f5f5f5; + --color-neutral-200: #e5e5e5; + --color-neutral-300: #d4d4d4; + --color-neutral-400: #a3a3a3; + --color-neutral-500: #737373; + --color-neutral-600: #525252; + --color-neutral-700: #404040; + --color-neutral-800: #262626; + --color-neutral-900: #171717; + + /* ------------------------------------------------------------------------ + SPACING — 4px base grid + ------------------------------------------------------------------------ */ + /* --space-N = N × 4px */ + + --space-1: 4px; /* 4px */ + --space-2: 8px; /* 8px */ + --space-3: 12px; /* 12px */ + --space-4: 16px; /* 16px */ + --space-5: 20px; /* 20px */ + --space-6: 24px; /* 24px */ + --space-7: 28px; /* 28px */ + --space-8: 32px; /* 32px */ + --space-9: 36px; /* 36px */ + --space-10: 40px; /* 40px */ + --space-11: 44px; /* 44px */ + --space-12: 48px; /* 48px */ + --space-14: 56px; /* 56px */ + --space-16: 64px; /* 64px */ + + /* ------------------------------------------------------------------------ + BORDER RADIUS + ------------------------------------------------------------------------ */ + + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 20px; + --radius-full: 9999px; + + /* ------------------------------------------------------------------------ + TRANSITIONS + ------------------------------------------------------------------------ */ + + --transition-fast: all 0.10s ease; + --transition-normal: all 0.20s ease; + --transition-slow: all 0.40s ease; + + /* Easing functions for fine-grained control */ + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + --ease-out: cubic-bezier(0, 0, 0.2, 1); + --ease-in: cubic-bezier(0.4, 0, 1, 1); + + --duration-fast: 100ms; + --duration-normal: 200ms; + --duration-slow: 400ms; + + /* ------------------------------------------------------------------------ + Z-INDEX SCALE + ------------------------------------------------------------------------ */ + + --z-dropdown: 1000; + --z-sticky: 1020; + --z-modal: 1050; + --z-toast: 1080; + + /* ------------------------------------------------------------------------ + BREAKPOINTS — Reference only (use in media queries, not calc()) + sm: 640px + md: 768px + lg: 1024px + xl: 1280px + ------------------------------------------------------------------------ */ + +} + + +/* ========================================================================== + THEME A — Industrial Robusto (Dark) + Usage: or + Style: Industrial, robust, high-contrast amber accents, clip-path diagonals + ========================================================================== */ + +[data-theme="industrial"] { + + /* ------------------------------------------------------------------------ + PRIMITIVE COLORS + ------------------------------------------------------------------------ */ + + --color-primary: #F5A623; /* Amber gold — main brand accent */ + --color-primary-hover: #e8951a; /* Darker amber on hover */ + --color-primary-active: #d4850f; /* Pressed state */ + --color-primary-muted: rgba(245, 166, 35, 0.15); /* Subtle tint */ + + --color-secondary: #333333; /* Mid-dark border / secondary bg */ + --color-secondary-hover: #444444; + + --color-accent: #F5A623; /* Same as primary in this theme */ + + /* ------------------------------------------------------------------------ + BACKGROUNDS + ------------------------------------------------------------------------ */ + + --color-bg-base: #0d0d0d; /* Page / app shell background */ + --color-bg-elevated: #1a1a1a; /* Cards, panels, sidebars */ + --color-bg-overlay: #252525; /* Modals, dropdowns, tooltips */ + + /* Surface levels (for layered UI) */ + --color-surface-1: #1a1a1a; /* Lowest raised surface */ + --color-surface-2: #252525; /* Mid-level surface */ + --color-surface-3: #303030; /* Highest raised surface */ + + /* ------------------------------------------------------------------------ + TEXT + ------------------------------------------------------------------------ */ + + --color-text-primary: #FFFFFF; + --color-text-secondary: #CCCCCC; + --color-text-muted: #888888; + --color-text-disabled: #555555; + --color-text-inverse: #000000; /* Text on amber background */ + --color-text-accent: #F5A623; + + /* ------------------------------------------------------------------------ + BORDERS + ------------------------------------------------------------------------ */ + + --color-border: #333333; + --color-border-strong: #555555; + --color-border-accent: #F5A623; + --color-border-focus: #F5A623; + + /* ------------------------------------------------------------------------ + BUTTONS + ------------------------------------------------------------------------ */ + + /* Primary button */ + --btn-primary-bg: #F5A623; + --btn-primary-bg-hover: #e8951a; + --btn-primary-bg-active: #d4850f; + --btn-primary-text: #000000; + --btn-primary-border: transparent; + + /* Secondary button */ + --btn-secondary-bg: transparent; + --btn-secondary-bg-hover: rgba(245, 166, 35, 0.10); + --btn-secondary-text: #F5A623; + --btn-secondary-border: #F5A623; + + /* Ghost / Danger */ + --btn-ghost-bg: transparent; + --btn-ghost-text: #CCCCCC; + --btn-ghost-border: #333333; + + --btn-danger-bg: #ef4444; + --btn-danger-text: #FFFFFF; + + /* ------------------------------------------------------------------------ + TYPOGRAPHY + ------------------------------------------------------------------------ */ + + /* Font families */ + --font-heading: 'Barlow Condensed', 'Arial Narrow', sans-serif; + --font-body: 'Barlow', 'Arial', sans-serif; + --font-mono: 'Courier New', 'Consolas', monospace; /* prices / SKUs */ + + /* Font weights */ + --font-weight-light: 300; /* n/a in Barlow — falls to 400 */ + --font-weight-regular: 400; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --font-weight-extrabold: 800; + + /* Heading weights (Barlow Condensed) */ + --heading-weight-primary: 800; + --heading-weight-secondary: 600; + + /* ------------------------------------------------------------------------ + SHADOWS / ELEVATION + Tinted with amber to feel cohesive with the theme + ------------------------------------------------------------------------ */ + + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.60), + 0 1px 2px rgba(0, 0, 0, 0.40); + + --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.60), + 0 2px 4px rgba(0, 0, 0, 0.40); + + --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.70), + 0 4px 6px rgba(0, 0, 0, 0.50); + + --shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.80), + 0 10px 10px rgba(0, 0, 0, 0.50); + + /* Accent glow — use on focused/highlighted elements */ + --shadow-accent: 0 0 0 3px rgba(245, 166, 35, 0.40); + --shadow-focus: 0 0 0 3px rgba(245, 166, 35, 0.50); + + /* ------------------------------------------------------------------------ + MISC UI + ------------------------------------------------------------------------ */ + + --scrollbar-track: #1a1a1a; + --scrollbar-thumb: #444444; + --scrollbar-thumb-hover: #F5A623; + + --overlay-backdrop: rgba(0, 0, 0, 0.75); + + /* Industrial clip-path angle (use in clip-path: polygon(...) utilities) */ + --clip-diagonal-angle: 6deg; + +} + + +/* ========================================================================== + THEME B — Técnico Moderno (Light) + Usage: or + Style: Clean, modern, Poppins typography, subtle dot-grid background + ========================================================================== */ + +[data-theme="modern"] { + + /* ------------------------------------------------------------------------ + PRIMITIVE COLORS + ------------------------------------------------------------------------ */ + + --color-primary: #FF6B35; /* Orange — main brand accent */ + --color-primary-hover: #f05a22; /* Darker on hover */ + --color-primary-active: #dc4a12; /* Pressed state */ + --color-primary-muted: rgba(255, 107, 53, 0.10); /* Subtle tint */ + + --color-secondary: #1a1a2e; /* Deep navy — used for strong text */ + --color-secondary-hover: #252545; + + --color-accent: #FF6B35; /* Same as primary in this theme */ + + /* ------------------------------------------------------------------------ + BACKGROUNDS + ------------------------------------------------------------------------ */ + + --color-bg-base: #FFFFFF; /* Page / app shell background */ + --color-bg-elevated: #F8F9FF; /* Cards, panels — very subtle blue */ + --color-bg-overlay: #FFFFFF; /* Modals, dropdowns */ + + /* Surface levels */ + --color-surface-1: #F8F9FF; + --color-surface-2: #F0F2FF; + --color-surface-3: #E8EBFF; + + /* Dot-grid background pattern (apply via background-image on body/shell) */ + /* background-image: radial-gradient(circle, var(--dot-grid-color) 1px, transparent 1px); */ + /* background-size: var(--dot-grid-size) var(--dot-grid-size); */ + --dot-grid-color: rgba(26, 26, 46, 0.07); + --dot-grid-size: 24px; + + /* ------------------------------------------------------------------------ + TEXT + ------------------------------------------------------------------------ */ + + --color-text-primary: #1a1a2e; + --color-text-secondary: #4a4a6a; + --color-text-muted: #8080a0; + --color-text-disabled: #b0b0c8; + --color-text-inverse: #FFFFFF; /* Text on orange background */ + --color-text-accent: #FF6B35; + + /* ------------------------------------------------------------------------ + BORDERS + ------------------------------------------------------------------------ */ + + --color-border: #e2e4f0; + --color-border-strong: #c8cadc; + --color-border-accent: #FF6B35; + --color-border-focus: #FF6B35; + + /* ------------------------------------------------------------------------ + BUTTONS + ------------------------------------------------------------------------ */ + + /* Primary button */ + --btn-primary-bg: #FF6B35; + --btn-primary-bg-hover: #f05a22; + --btn-primary-bg-active: #dc4a12; + --btn-primary-text: #FFFFFF; + --btn-primary-border: transparent; + + /* Secondary button */ + --btn-secondary-bg: transparent; + --btn-secondary-bg-hover: rgba(255, 107, 53, 0.08); + --btn-secondary-text: #FF6B35; + --btn-secondary-border: #FF6B35; + + /* Ghost / Danger */ + --btn-ghost-bg: transparent; + --btn-ghost-text: #4a4a6a; + --btn-ghost-border: #e2e4f0; + + --btn-danger-bg: #ef4444; + --btn-danger-text: #FFFFFF; + + /* ------------------------------------------------------------------------ + TYPOGRAPHY + ------------------------------------------------------------------------ */ + + /* Font families */ + --font-heading: 'Poppins', 'Segoe UI', sans-serif; + --font-body: 'Poppins', 'Segoe UI', sans-serif; + --font-mono: 'Courier New', 'Consolas', monospace; /* prices / SKUs */ + + /* Font weights */ + --font-weight-light: 300; + --font-weight-regular: 400; + --font-weight-semibold: 600; + --font-weight-bold: 700; + --font-weight-extrabold: 800; /* falls to 700 in Poppins */ + + /* Heading weights (Poppins) */ + --heading-weight-primary: 700; + --heading-weight-secondary: 600; + + /* ------------------------------------------------------------------------ + SHADOWS / ELEVATION + Softer, cooler tints for the light theme + ------------------------------------------------------------------------ */ + + --shadow-sm: 0 1px 3px rgba(26, 26, 46, 0.08), + 0 1px 2px rgba(26, 26, 46, 0.05); + + --shadow-md: 0 4px 6px rgba(26, 26, 46, 0.08), + 0 2px 4px rgba(26, 26, 46, 0.05); + + --shadow-lg: 0 10px 15px rgba(26, 26, 46, 0.10), + 0 4px 6px rgba(26, 26, 46, 0.06); + + --shadow-xl: 0 20px 25px rgba(26, 26, 46, 0.12), + 0 10px 10px rgba(26, 26, 46, 0.06); + + /* Accent glow — use on focused/highlighted elements */ + --shadow-accent: 0 0 0 3px rgba(255, 107, 53, 0.25); + --shadow-focus: 0 0 0 3px rgba(255, 107, 53, 0.30); + + /* ------------------------------------------------------------------------ + MISC UI + ------------------------------------------------------------------------ */ + + --scrollbar-track: #F8F9FF; + --scrollbar-thumb: #c8cadc; + --scrollbar-thumb-hover: #FF6B35; + + --overlay-backdrop: rgba(26, 26, 46, 0.50); + + /* No diagonal clip in modern theme — set to 0 for override-safe utilities */ + --clip-diagonal-angle: 0deg; + +} + + +/* ========================================================================== + TYPOGRAPHY SCALE — Token definitions + Resolved at theme level because font families differ between themes. + These tokens map to semantic roles and should be consumed directly. + ========================================================================== */ + +/* Shared scale values (dimensionless, theme-independent) */ +:root { + + /* --- Type scale (font-size) --- */ + --text-h1: clamp(2.25rem, 5vw, 3.5rem); /* 36px → 56px */ + --text-h2: clamp(1.875rem, 4vw, 2.75rem); /* 30px → 44px */ + --text-h3: clamp(1.5rem, 3vw, 2.125rem); /* 24px → 34px */ + --text-h4: clamp(1.25rem, 2vw, 1.625rem); /* 20px → 26px */ + --text-h5: 1.125rem; /* 18px */ + --text-h6: 1rem; /* 16px */ + + --text-body-lg: 1.125rem; /* 18px */ + --text-body: 1rem; /* 16px */ + --text-body-sm: 0.875rem; /* 14px */ + --text-caption: 0.75rem; /* 12px */ + --text-label: 0.8125rem; /* 13px */ + --text-mono: 1rem; /* 16px — prices, SKUs */ + + /* --- Line heights --- */ + --leading-h1: 1.10; + --leading-h2: 1.12; + --leading-h3: 1.15; + --leading-h4: 1.20; + --leading-h5: 1.25; + --leading-h6: 1.30; + + --leading-body-lg: 1.65; + --leading-body: 1.60; + --leading-body-sm: 1.55; + --leading-caption: 1.45; + --leading-label: 1.40; + --leading-mono: 1.50; + + /* --- Letter spacing --- */ + --tracking-tight: -0.03em; + --tracking-snug: -0.01em; + --tracking-normal: 0em; + --tracking-wide: 0.03em; + --tracking-wider: 0.06em; + --tracking-widest: 0.12em; /* Use for ALL-CAPS labels / badges */ + +} + +/* Heading letter-spacing per theme */ +[data-theme="industrial"] { + --heading-tracking-h1: -0.02em; + --heading-tracking-h2: -0.02em; + --heading-tracking-h3: -0.01em; + --heading-tracking-h4: 0em; + --heading-tracking-h5: 0.02em; + --heading-tracking-h6: 0.04em; +} + +[data-theme="modern"] { + --heading-tracking-h1: -0.03em; + --heading-tracking-h2: -0.02em; + --heading-tracking-h3: -0.01em; + --heading-tracking-h4: 0em; + --heading-tracking-h5: 0em; + --heading-tracking-h6: 0.01em; +} + + +/* ========================================================================== + COMPONENT SHORTHAND TOKENS + Convenience aliases that combine multiple primitives. Components should + reference these rather than the primitives above. + ========================================================================== */ + +:root { + + /* --- Input / form fields --- */ + /* These are intentionally left as CSS variable references so they resolve + correctly within whichever theme is active at runtime. */ + + /* (No :root overrides needed — components consume --color-* directly.) */ + + /* --- Focus ring --- */ + --focus-ring: 0 0 0 3px var(--shadow-focus, rgba(245,166,35,0.40)); + + /* --- Content max widths --- */ + --content-xs: 480px; + --content-sm: 640px; + --content-md: 768px; + --content-lg: 1024px; + --content-xl: 1280px; + --content-full: 100%; + +} + + +/* ========================================================================== + UTILITY — Scrollbar styles (opt-in via class) + ========================================================================== */ + +.themed-scrollbar { + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); +} + +.themed-scrollbar::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.themed-scrollbar::-webkit-scrollbar-track { + background: var(--scrollbar-track); +} + +.themed-scrollbar::-webkit-scrollbar-thumb { + background-color: var(--scrollbar-thumb); + border-radius: var(--radius-full); + border: 2px solid var(--scrollbar-track); +} + +.themed-scrollbar::-webkit-scrollbar-thumb:hover { + background-color: var(--scrollbar-thumb-hover); +} + + +/* ========================================================================== + UTILITY — Dot-grid background (Theme B helper) + Apply class .bg-dot-grid to body or layout shell when using modern theme. + ========================================================================== */ + +[data-theme="modern"] .bg-dot-grid { + background-color: var(--color-bg-base); + background-image: radial-gradient( + circle, + var(--dot-grid-color) 1px, + transparent 1px + ); + background-size: var(--dot-grid-size) var(--dot-grid-size); +} + + +/* ========================================================================== + UTILITY — Industrial diagonal clip helpers (Theme A) + ========================================================================== */ + +[data-theme="industrial"] .clip-top-right { + clip-path: polygon(0 0, calc(100% - 24px) 0, 100% 24px, 100% 100%, 0 100%); +} + +[data-theme="industrial"] .clip-bottom-left { + clip-path: polygon(0 0, 100% 0, 100% 100%, 24px 100%, 0 calc(100% - 24px)); +} + +[data-theme="industrial"] .clip-corner { + clip-path: polygon(0 0, calc(100% - 16px) 0, 100% 16px, 100% 100%, 0 100%); +} + + +/* ========================================================================== + GLASSMORPHISM TOKENS + ========================================================================== */ + +[data-theme="industrial"] { + --glass-bg: rgba(26, 26, 26, 0.70); + --glass-bg-strong: rgba(26, 26, 26, 0.85); + --glass-border: rgba(255, 255, 255, 0.08); + --glass-blur: 16px; + --glass-highlight: rgba(245, 166, 35, 0.06); + + --glow-color: rgba(245, 166, 35, 0.40); + --glow-color-soft: rgba(245, 166, 35, 0.15); + --glow-color-strong: rgba(245, 166, 35, 0.60); + + --gradient-accent: linear-gradient(135deg, #F5A623 0%, #e8951a 50%, #d4850f 100%); + --gradient-text: linear-gradient(135deg, #F5A623 0%, #FFD080 50%, #F5A623 100%); + + --canvas-grid-color: rgba(255, 255, 255, 0.06); + --canvas-star-color: rgba(245, 166, 35, 0.30); + --canvas-glow-color: rgba(245, 166, 35, 0.08); +} + +[data-theme="modern"] { + --glass-bg: rgba(248, 249, 255, 0.70); + --glass-bg-strong: rgba(248, 249, 255, 0.85); + --glass-border: rgba(26, 26, 46, 0.08); + --glass-blur: 16px; + --glass-highlight: rgba(255, 107, 53, 0.04); + + --glow-color: rgba(255, 107, 53, 0.35); + --glow-color-soft: rgba(255, 107, 53, 0.12); + --glow-color-strong: rgba(255, 107, 53, 0.55); + + --gradient-accent: linear-gradient(135deg, #FF6B35 0%, #FF8F65 50%, #FF6B35 100%); + --gradient-text: linear-gradient(135deg, #FF6B35 0%, #FF8F65 50%, #e85520 100%); + + --canvas-grid-color: rgba(26, 26, 46, 0.05); + --canvas-star-color: rgba(255, 107, 53, 0.20); + --canvas-glow-color: rgba(255, 107, 53, 0.06); +} + + +/* ========================================================================== + ANIMATION KEYFRAMES + ========================================================================== */ + +@keyframes nx-fade-up { + from { opacity: 0; transform: translateY(24px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes nx-glow-pulse { + 0%, 100% { box-shadow: 0 0 20px var(--glow-color-soft); } + 50% { box-shadow: 0 0 40px var(--glow-color); } +} + +@keyframes nx-shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + + +/* ========================================================================== + END OF TOKENS FILE + nexus-autoparts-design/tokens/tokens.css + ========================================================================== */ diff --git a/pos/static/js/accounting.min.js b/pos/static/js/accounting.min.js new file mode 100644 index 0000000..19f0acc --- /dev/null +++ b/pos/static/js/accounting.min.js @@ -0,0 +1 @@ +const Accounting=(()=>{function a(){return localStorage.getItem("pos_token")||""}async function e(e,n={}){const t=await fetch(`/pos/api/accounting${e}`,{headers:{Authorization:`Bearer ${a()}`,"Content-Type":"application/json"},...n});if(!t.ok){const a=await t.json().catch((()=>({error:t.statusText})));throw new Error(a.error||"Request failed")}return t.json()}function n(a){return parseFloat(a||0).toLocaleString("es-MX",{minimumFractionDigits:2,maximumFractionDigits:2})}function t(a){document.querySelectorAll(".tab-btn").forEach((a=>{a.classList.remove("is-active"),a.setAttribute("aria-selected","false")})),document.querySelectorAll(".tab-panel").forEach((a=>a.classList.remove("is-active")));const e=document.querySelector(`.tab-btn[onclick*="'${a}'"]`);e&&(e.classList.add("is-active"),e.setAttribute("aria-selected","true"));const n=document.getElementById(`panel-${a}`);n&&n.classList.add("is-active"),"cxc"===a&&o(),"cxp"===a&&c(),"balance"===a&&s(),"resultados"===a&&i(),"flujo"===a&&l(),"conciliacion"===a&&d(),"cierre"===a&&p()}function r(a,e){return`${e||a}`}async function o(){const a=document.getElementById("panel-cxc");if(!a)return;const t=a.querySelector(".data-table tbody");if(t)try{const o=await e("/aging"),c=o.data||[];if(!c.length)return void(t.innerHTML='No hay cuentas por cobrar.');t.innerHTML=c.map((a=>{const e=a.days_overdue>0?"overdue":a.paid>0&&a.balance>0?"partial":a.balance<=0?"ok":"pending",t="overdue"===e?"Vencida":"partial"===e?"Parcial":"ok"===e?"Pagada":"Vigente";return`\n ${a.invoice||a.folio||"-"}\n ${a.name||a.customer_name||"-"}\n ${a.issue_date?new Date(a.issue_date).toLocaleDateString("es-MX"):"-"}\n ${a.due_date?new Date(a.due_date).toLocaleDateString("es-MX"):"-"}\n $${n(a.total)}\n $${n(a.paid||0)}\n $${n(a.balance||a.total)}\n ${r(e,t)}\n \n `})).join("");const s=a.querySelector(".pagination span");s&&(s.textContent=`Mostrando 1-${c.length} de ${o.totals?.count||c.length} registros`),function(a,e){const n=document.querySelector(`.tab-btn[onclick*="'${a}'"]`);if(!n)return;const t=n.querySelector(".tab-btn__badge");t&&(t.textContent=e)}("cxc",c.length)}catch(a){t.innerHTML=`Error: ${a.message}`}}async function c(){const a=document.getElementById("panel-cxp");if(!a)return;const t=a.querySelector(".data-table tbody");if(t)try{const o=await e("/aging?type=payable"),c=o.data||[];if(!c.length)return void(t.innerHTML='No hay cuentas por pagar.');t.innerHTML=c.map((a=>{const e=a.days_overdue>0?"overdue":a.paid>0&&a.balance>0?"partial":a.balance<=0?"ok":"pending",t="overdue"===e?"Vencida":"partial"===e?"Parcial":"ok"===e?"Pagada":"Vigente";return`\n ${a.invoice||a.folio||"-"}\n ${a.name||a.vendor_name||"-"}\n ${a.receipt_date?new Date(a.receipt_date).toLocaleDateString("es-MX"):"-"}\n ${a.due_date?new Date(a.due_date).toLocaleDateString("es-MX"):"-"}\n $${n(a.total)}\n $${n(a.paid||0)}\n $${n(a.balance||a.total)}\n ${r(e,t)}\n \n `})).join("");const s=a.querySelector(".pagination span");s&&(s.textContent=`Mostrando 1-${c.length} de ${o.totals?.count||c.length} registros`)}catch(a){t.innerHTML=`Error: ${a.message}`}}async function s(){const a=document.getElementById("panel-balance");if(!a)return;const t=a.querySelector(".finance-grid");if(t)try{const r=new Date,o=await e(`/balance-sheet?date=${r.toISOString().slice(0,10)}`),c=t.querySelector(".finance-card:first-child"),s=t.querySelector(".finance-card:last-child");if(c){let a='
Activos
';if(o.activo&&o.activo.items)for(const e of o.activo.items){const t=e.balance<0;a+=`
\n ${e.code?e.code+" ":""}${e.name}\n $${n(e.balance)}\n
`}a+=`
\n Total Activos\n $${n(o.activo?.total||0)}\n
`,c.innerHTML=a}if(s){let a='
Pasivo + Capital
';if(o.pasivo&&o.pasivo.items){a+='
Pasivo
';for(const e of o.pasivo.items)a+=`
\n ${e.code?e.code+" ":""}${e.name}\n $${n(e.balance)}\n
`;a+=`
Total Pasivo$${n(o.pasivo.total)}
`}if(o.capital&&o.capital.items){a+='
Capital Contable
';for(const e of o.capital.items){const t=e.balance>0;a+=`
\n ${e.code?e.code+" ":""}${e.name}\n $${n(e.balance)}\n
`}a+=`
Total Capital$${n(o.capital.total)}
`}a+=`
\n Total Pasivo + Capital\n $${n((o.pasivo?.total||0)+(o.capital?.total||0))}\n
`,s.innerHTML=a}const i=a.querySelector(".select-filter");i&&o.as_of&&(i.innerHTML=``)}catch(a){t.innerHTML=`

Error: ${a.message}

`}}async function i(){const a=document.getElementById("panel-resultados");if(!a)return;const t=a.querySelector(".finance-grid");if(t)try{const a=new Date,r=await e(`/income-statement?year=${a.getFullYear()}&month=${a.getMonth()+1}`),o=t.querySelector(".finance-card");if(!o)return;let c='
Estado de Resultados
';if(c+='
Ingresos
',r.ingresos&&r.ingresos.items)for(const a of r.ingresos.items)c+=`
\n ${a.name}\n $${n(a.amount)}\n
`;if(c+=`
Total Ingresos$${n(r.ingresos?.total||0)}
`,c+='
Costo de Ventas
',r.costos&&r.costos.items)for(const a of r.costos.items)c+=`
\n ${a.name}\n -$${n(Math.abs(a.amount))}\n
`;if(c+=`
Utilidad Bruta$${n(r.utilidad_bruta||0)}
`,c+='
Gastos de Operacion
',r.gastos&&r.gastos.items)for(const a of r.gastos.items)c+=`
\n ${a.name}\n -$${n(Math.abs(a.amount))}\n
`;c+=`
Total Gastos Operacion-$${n(Math.abs(r.gastos?.total||0))}
`;c+=`
\n Utilidad Neta\n $${n(r.utilidad_neta||0)}\n
`,o.innerHTML=c}catch(a){t.innerHTML=`

Error: ${a.message}

`}}async function l(){}async function d(){}async function p(){const a=document.getElementById("panel-cierre");if(!a)return;const n=a.querySelector(".btn--primary");n&&!n.dataset.wired&&(n.dataset.wired="true",n.addEventListener("click",(async()=>{const a=new Date,n=a.getFullYear(),t=a.getMonth()+1;if(confirm(`Cerrar periodo ${t}/${n}? Esta accion no se puede revertir.`))try{await e("/periods/close",{method:"POST",body:JSON.stringify({year:n,month:t})}),alert("Periodo cerrado exitosamente.")}catch(a){alert("Error: "+a.message)}})))}function u(){for(var a=document.querySelectorAll("table"),e=null,n=0;n×',a.appendChild(e)}async function m(){const a=document.getElementById("entryDate").value,n=document.getElementById("entryType").value,t=document.getElementById("entryDescription").value.trim(),r=document.getElementById("entryResult");if(!a||!t)return void(r.innerHTML='Fecha y descripcion son obligatorios.');const o=[];if(document.querySelectorAll("#entryLines .entry-line").forEach((a=>{const e=a.querySelector(".entry-account").value.trim(),n=parseFloat(a.querySelector(".entry-debit").value)||0,t=parseFloat(a.querySelector(".entry-credit").value)||0;e&&(n||t)&&o.push({account:e,debit:n,credit:t})})),o.length)try{await e("/entries",{method:"POST",body:JSON.stringify({date:a,type:n,description:t,lines:o})}),r.innerHTML='Poliza creada exitosamente.',setTimeout((()=>v()),1200)}catch(a){r.innerHTML='Error: '+a.message+""}else r.innerHTML='Agregue al menos una partida.'}return document.addEventListener("DOMContentLoaded",(function(){(a()||(window.location.href="/pos/login",0))&&(!function(){const a=document.getElementById("live-clock");if(!a)return;const e=()=>{const e=new Date;a.textContent=e.toLocaleTimeString("es-MX",{hour:"2-digit",minute:"2-digit"})};e(),setInterval(e,1e3)}(),async function(){if(!(document.querySelectorAll(".summary-card").length<4))try{const a=new Date;await e(`/trial-balance?year=${a.getFullYear()}&month=${a.getMonth()+1}`)}catch(a){}}(),o())})),window.switchTab=t,window.exportarContabilidad=u,window.showNewEntryModal=f,window.closeNewEntryModal=v,window.addEntryLine=_,window.submitNewEntry=m,{switchTab:t,loadAging:o,loadAccountsPayable:c,loadBalanceSheet:s,loadIncomeStatement:i,loadCashFlow:l,loadReconciliation:d,loadPeriodClose:p,exportarContabilidad:u,showNewEntryModal:f,closeNewEntryModal:v,addEntryLine:_,submitNewEntry:m}})(); \ No newline at end of file diff --git a/pos/static/js/app-init.min.js b/pos/static/js/app-init.min.js new file mode 100644 index 0000000..1f8c0c6 --- /dev/null +++ b/pos/static/js/app-init.min.js @@ -0,0 +1 @@ +!function(){"use strict";var e=localStorage.getItem("pos_token");if(e){try{var t=JSON.parse(atob(e.split(".")[1]));if(1e3*t.exp' + ? '' + esc(p.name) + '' : ''; var tierClass = p.priority_tier === 1 ? ' part-card--tier1' : (p.priority_tier === 2 ? ' part-card--tier2' : ''); @@ -997,7 +997,7 @@ : ''; var imgHtml = p.image_url - ? '' + esc(p.name) + '' + ? '' + esc(p.name) + '' : ''; // Local-mode extras: manufacturer badge + priority tier indicator @@ -1125,7 +1125,7 @@ html += '
' + esc(p.oem_part_number) + '
'; html += '
' + esc(p.name) + '
'; if (p.description) html += '
' + esc(p.description) + '
'; - if (p.image_url) html += '
'; + if (p.image_url) html += '
'; html += ''; // Local stock diff --git a/pos/static/js/catalog.min.js b/pos/static/js/catalog.min.js new file mode 100644 index 0000000..6468fea --- /dev/null +++ b/pos/static/js/catalog.min.js @@ -0,0 +1 @@ +!function(){"use strict";var e="/pos/api/catalog",a=localStorage.getItem("pos_token");if(a){var t={Authorization:"Bearer "+a,"Content-Type":"application/json"},n=document.getElementById("breadcrumb"),o=document.getElementById("searchInput"),i=document.getElementById("searchDropdown"),r=document.getElementById("levelTitle"),s=document.getElementById("levelFilter"),l=document.getElementById("loading"),d=document.getElementById("emptyState"),c=document.getElementById("emptyTitle"),u=document.getElementById("emptySubtitle"),p=document.getElementById("navGrid"),m=document.getElementById("partsGrid"),v=document.getElementById("pagination"),g=document.getElementById("pageBody"),y=document.getElementById("detailPanel"),f=document.getElementById("detailOverlay"),_=document.getElementById("detailBody"),h=document.getElementById("detailFooter"),b=document.getElementById("detailClose"),x=document.getElementById("qtyMinus"),E=document.getElementById("qtyPlus"),k=document.getElementById("qtyDisplay"),I=document.getElementById("addToCartBtn"),T=document.getElementById("cartSidebar"),S=document.getElementById("cartOverlay"),L=document.getElementById("cartItems"),C=document.getElementById("cartEmpty"),M=document.getElementById("cartSubtotal"),B=document.getElementById("cartTax"),w=document.getElementById("cartTotal"),N=document.getElementById("cartBadge"),A=document.getElementById("checkoutBtn"),H=document.getElementById("cartFab"),P=document.getElementById("cartCloseBtn"),j={level:"brands",brand:null,model:null,year:null,engine:null,category:null,group:null,partType:null,nxGroup:null,nxSubgroup:null,nxPartType:null},q="local";localStorage.setItem("catalog_mode","local");var O=1,G=null,U=1,R=!1;window.addEventListener("popstate",(function(e){e.state&&(R=!0,j.level=e.state.level||"brands",j.brand=e.state.brand,j.model=e.state.model,j.year=e.state.year,j.engine=e.state.engine,j.category=e.state.category,j.group=e.state.group,j.partType=e.state.partType||null,j.nxGroup=e.state.nxGroup||null,j.nxSubgroup=e.state.nxSubgroup||null,j.nxPartType=e.state.nxPartType||null,O=e.state.page||1,"brands"===j.level?he():"models"===j.level?be():"years"===j.level?xe():"engines"===j.level?Ee():"categories"===j.level?Te():"groups"===j.level?j.nxGroup?Se():ke():"part_types"===j.level?j.nxSubgroup?Le():Ie():"parts"===j.level?Me(O):he(),R=!1)})),history.replaceState(JSON.parse(JSON.stringify(j)),"","/pos/catalog");var $=JSON.parse(localStorage.getItem("pos_cart")||"[]"),V=null;b.addEventListener("click",Ne),f.addEventListener("click",Ne),x.addEventListener("click",(function(){U>1&&(U--,k.textContent=U)})),E.addEventListener("click",(function(){U++,k.textContent=U})),I.addEventListener("click",(function(){if(G){var e=G.part,a=G.local;a&&(qe({id:e.id_part,part_number:e.oem_part_number,name:e.name,brand:"",price:a.price_1,tax_rate:a.tax_rate||.16,unit:a.unit||"PZA",stock:a.stock,source:"local",inventory_id:a.inventory_id},U),Ne())}}));var z=null,J=null;o.addEventListener("input",(function(){clearTimeout(z);var e=this.value.trim();(He(e.length>=3?Ae(e):null),e.length<2)?i.classList.remove("is-visible"):"keyword"===Ae(e)&&(z=setTimeout((function(){je(e)}),350))})),o.addEventListener("keydown",(function(e){if("Enter"===e.key){e.preventDefault(),clearTimeout(z);var a=this.value.trim();a.length>=2&&Pe(a)}"Escape"===e.key&&(i.classList.remove("is-visible"),He(null))})),document.addEventListener("click",(function(e){i.contains(e.target)||e.target===o||i.classList.remove("is-visible")})),L.addEventListener("click",(function(e){var a=e.target.closest("[data-cart-action]");if(a){var t=parseInt(a.dataset.idx),n=a.dataset.cartAction;"dec"===n?Ge(t,$[t].quantity-1):"inc"===n?Ge(t,$[t].quantity+1):"remove"===n&&Oe(t)}})),H.addEventListener("click",$e),P.addEventListener("click",$e),S.addEventListener("click",$e),A.addEventListener("click",Ve);var D="",Z=null;document.addEventListener("keydown",(function(e){if("F1"===e.key)return e.preventDefault(),void o.focus();if("Escape"===e.key)return Ne(),void(T.classList.contains("open")&&$e());if("INPUT"!==document.activeElement.tagName&&"TEXTAREA"!==document.activeElement.tagName){if("Enter"===e.key&&D.length>=4){var a=D.trim();return D="",o.value=a,void je(a)}1===e.key.length&&(D+=e.key,clearTimeout(Z),Z=setTimeout((function(){D=""}),200))}})),document.querySelectorAll("[data-theme-switch]").forEach((function(e){e.addEventListener("click",(function(){var e=this.dataset.themeSwitch;document.documentElement.setAttribute("data-theme",e),localStorage.setItem("pos_theme",e),document.querySelectorAll("[data-theme-switch]").forEach((function(e){e.classList.remove("is-active"),e.setAttribute("aria-pressed","false")})),this.classList.add("is-active"),this.setAttribute("aria-pressed","true")}));var a=localStorage.getItem("pos_theme")||"industrial";e.dataset.themeSwitch===a?(e.classList.add("is-active"),e.setAttribute("aria-pressed","true")):(e.classList.remove("is-active"),e.setAttribute("aria-pressed","false"))}));var F=document.getElementById("vsYear"),Y=document.getElementById("vsBrand"),Q=document.getElementById("vsModel"),W=document.getElementById("vsEngine"),X=document.getElementById("vsClear"),K=document.getElementById("plateInputWrap"),ee=document.getElementById("plateInput"),ae=document.getElementById("plateStatus"),te=document.getElementById("plateToggle");ee&&ee.addEventListener("keydown",(function(e){"Enter"===e.key&&(e.preventDefault(),Fe())}));var ne=document.getElementById("vinInputWrap"),oe=document.getElementById("vinInput"),ie=document.getElementById("vinStatus"),re=document.getElementById("vinToggle");oe&&oe.addEventListener("keydown",(function(e){"Enter"===e.key&&(e.preventDefault(),Ye())})),window.CatalogApp={toggleCart:$e,goToCheckout:Ve,addToCart:qe,removeFromCart:Oe,updateQty:Ge,clearCart:function(){$=[],Ue(),Re()},loadPage:function(e){Me(e)},vsYearChanged:function(){var a=F.value;Y.innerHTML='',Q.innerHTML='',W.innerHTML='',Y.disabled=!0,Q.disabled=!0,W.disabled=!0,X.style.display=a?"":"none",a&&(Y.disabled=!1,de(e+"/brands?year_id="+a+"&mode="+q).then((function(e){var a=e.data||e;a&&(Y.innerHTML=''+a.map((function(e){return'"})).join(""))})))},vsBrandChanged:function(){var a=Y.value,t=F.value;Q.innerHTML='',W.innerHTML='',Q.disabled=!0,W.disabled=!0,a&&(Q.disabled=!1,de(e+"/models?brand_id="+a+(t?"&year_id="+t:"")).then((function(e){var a=e.data||e;a&&(Q.innerHTML=''+a.map((function(e){return'"})).join(""))})))},vsModelChanged:function(){var a=Q.value,t=F.value;W.innerHTML='',W.disabled=!0,a&&t&&(W.disabled=!1,de(e+"/engines?model_id="+a+"&year_id="+t).then((function(e){var a=e.data||e;a&&(W.innerHTML=''+a.map((function(e){var a=e.name_engine+(e.trim_level?" ("+e.trim_level+")":"");return'"})).join(""),1===a.length&&(W.value=a[0].id_mye,Je()))})))},vsEngineChanged:Je,vsClear:De,startBarcodeScan:function(){window.NexusNative?window.NexusNative.scanBarcode().then((function(e){e&&(o.value=e,je(e))})):alert("El modulo de escaneo no esta cargado.")},toggleVin:function(){var e="none"!==ne.style.display;ne.style.display=e?"none":"",re.textContent=e?"Tienes el VIN?":"Ocultar VIN",!e&&oe&&oe.focus()},decodeVin:Ye,togglePlate:function(){var e="none"!==K.style.display;K.style.display=e?"none":"",te.textContent=e?"Tienes las placas?":"Ocultar placas",!e&&ee&&ee.focus()},lookupPlate:Fe,setMode:function(a){if("oem"===a||"local"===a||"supplies"===a)if("oem"!==a){if(a!==q){if(q=a,localStorage.setItem("catalog_mode",a),se(),j.category=j.group=j.partType=null,j.nxGroup=j.nxSubgroup=j.nxPartType=null,O=1,"supplies"===a){try{De()}catch(e){}return j.brand=j.model=j.year=j.engine=null,j.level="categories",j.level="categories",le(),ge(),r.textContent="Shop Supplies (sin vehiculo)",_e(!0),ce(),void de(e+"/shop-supplies/groups").then((function(a){ue(),a&&a.data&&a.data.length?(p.className="nav-grid",p.innerHTML=a.data.map((function(e){return'"})).join(""),p.querySelectorAll(".nav-card").forEach((function(a){a.addEventListener("click",(function(){j.nxGroup={slug:this.dataset.slug,name:this.dataset.name},j.nxSubgroup=null,j.nxPartType=null,j.level="groups",le(),ge(),r.textContent=j.nxGroup.name,_e(!0),ce(),de(e+"/shop-supplies/subgroups?group_slug="+encodeURIComponent(j.nxGroup.slug)).then((function(a){ue(),a&&a.data&&a.data.length?(p.className="nav-grid",p.innerHTML=a.data.map((function(e){return'"})).join(""),p.querySelectorAll(".nav-card").forEach((function(a){a.addEventListener("click",(function(){j.nxSubgroup={slug:this.dataset.slug,name:this.dataset.name},j.nxPartType=null,function(){j.level="part_types",j.nxPartType=null,le(),ge(),r.textContent=j.nxSubgroup.name,_e(!0),ce();var a=e+"/shop-supplies/part-types?group_slug="+encodeURIComponent(j.nxGroup.slug)+"&subgroup_slug="+encodeURIComponent(j.nxSubgroup.slug);de(a).then((function(e){if(ue(),e&&e.data&&e.data.length){if(1===e.data.length){var a=e.data[0];return j.nxPartType={slug:a.slug,name:a.name},void Ce(1)}p.className="nav-grid",p.innerHTML=e.data.map((function(e){var a=e.sample_image?'':"";return'"})).join(""),p.querySelectorAll(".nav-card").forEach((function(e){e.addEventListener("click",(function(){j.nxPartType={slug:this.dataset.slug,name:this.dataset.name},Ce(1)}))}))}else pe("Sin tipos","No hay tipos de parte en este subgrupo.")}))}()}))}))):pe("Sin subgrupos","No hay subgrupos con partes disponibles.")}))}))}))):pe("Sin supplies","No hay grupos de Shop Supplies disponibles.")}))}if(!(!j.engine||!j.engine.id_mye))return j.level="categories",void Te();try{De()}catch(e){}j.level="brands",j.brand=j.model=j.year=j.engine=null,he()}}else alert("Catálogo OEM próximamente. Por favor usa el modo Local o Shop Supplies.")}},Re(),se(),function(){var a="nexus:years-all",t=sessionStorage.getItem(a);if(t){var n=JSON.parse(t),o=n.data||n||[];if(!o.length){o=[];for(var i=2026;i>=1990;i--)o.push({id_year:i,year_car:i})}F.innerHTML=''+o.map((function(e){return'"})).join("")}else de(e+"/years-all").then((function(e){if(e){var t=e.data||e;if(!t||!t.length){t=[];for(var n=2026;n>=1990;n--)t.push({id_year:n,year_car:n})}sessionStorage.setItem(a,JSON.stringify(e)),F.innerHTML=''+t.map((function(e){return'"})).join("")}})).catch((function(){for(var e=[],a=2026;a>=1990;a--)e.push(a);F.innerHTML=''+e.map((function(e){return'"})).join("")}))}(),he()}else window.location.href="/pos/login";function se(){document.querySelectorAll("#modeToggle button").forEach((function(e){e.getAttribute("data-mode")===q?e.classList.add("is-active"):e.classList.remove("is-active")}))}function le(){if(!R){var e=JSON.parse(JSON.stringify(j));e.page=O,history.pushState(e,"","/pos/catalog")}}function de(e){V&&(V.abort(),V=null);var a={headers:t};return(0===e.indexOf("/pos/api/")&&-1!==e.indexOf("mode=")||-1!==e.indexOf("/years")||-1!==e.indexOf("/brands")||-1!==e.indexOf("/models")||-1!==e.indexOf("/engines")||-1!==e.indexOf("/categories")||-1!==e.indexOf("/groups")||-1!==e.indexOf("/part-types")||-1!==e.indexOf("/parts")||-1!==e.indexOf("/search"))&&(V=new AbortController,a.signal=V.signal),fetch(e,a).then((function(e){return 401===e.status?(localStorage.removeItem("pos_token"),window.location.href="/pos/login",null):e.json()})).catch((function(e){return"AbortError"===e.name||console.error("API error:",e),null}))}function ce(){l.classList.add("is-visible"),p.innerHTML="",m.style.display="none",m.innerHTML="",d.classList.remove("is-visible"),v.innerHTML="";var e=document.getElementById("diagLink");e&&"categories"!==j.level&&(e.style.display="none")}function ue(){l.classList.remove("is-visible")}function pe(e,a){c.textContent=e,u.textContent=a||"",d.classList.add("is-visible"),p.innerHTML="",m.style.display="none"}function me(e){return(parseFloat(e)||0).toFixed(2)}function ve(e){if(!e)return"";var a=document.createElement("div");return a.textContent=e,a.innerHTML}function ge(){var e=[];e.push({label:"Catalogo",action:"loadBrands"}),j.brand&&e.push({label:j.brand.name,action:"loadModels"}),j.model&&e.push({label:j.model.name,action:"loadYears"}),j.year&&e.push({label:String(j.year.year),action:"loadEngines"}),j.engine&&e.push({label:j.engine.name,action:"loadCategories"}),j.nxGroup?e.push({label:j.nxGroup.name,action:"loadNxSubgroups"}):j.category&&e.push({label:j.category.name,action:"loadGroups"}),j.nxSubgroup?e.push({label:j.nxSubgroup.name,action:"loadNxPartTypes"}):j.group&&e.push({label:j.group.name,action:"loadPartTypes"}),j.nxPartType?e.push({label:j.nxPartType.name,action:null}):j.partType&&e.push({label:j.partType.name,action:null});for(var a="",t=0;t0&&(a+=''),t'+ve(e[t].label)+"":a+=''+ve(e[t].label)+"";n.innerHTML=a,n.querySelectorAll("[data-bc-action]").forEach((function(e){e.addEventListener("click",(function(){var e=this.dataset.bcAction;"loadBrands"===e?(ye(),he()):"loadModels"===e?(fe("models"),be()):"loadYears"===e?(fe("years"),xe()):"loadEngines"===e?(fe("engines"),Ee()):"loadCategories"===e?(fe("categories"),Te()):"loadGroups"===e?(fe("groups"),ke()):"loadPartTypes"===e?(fe("part_types"),Ie()):"loadNxSubgroups"===e?(fe("groups"),Se()):"loadNxPartTypes"===e&&(fe("part_types"),Le())}))}))}function ye(){j.level="brands",le(),j.brand=j.model=j.year=j.engine=j.category=j.group=j.partType=null,j.nxGroup=j.nxSubgroup=j.nxPartType=null}function fe(e){var a=["brands","models","years","engines","categories","groups","part_types","parts"].indexOf(e);if(a<=0)ye();else{j.level=e;for(var t=[null,["model"],["year"],["engine"],["category","nxGroup"],["group","nxSubgroup"],["partType","nxPartType"],null],n=a;n=0?"":"none"}))}}function he(){j.level="brands",le(),ge(),r.textContent="Selecciona una marca",_e(!0),ce();var a="nexus:brands:"+q,t=sessionStorage.getItem(a);if(t)return ue(),void function(e){if(!e||!e.data||!e.data.length)return e?void pe("Sin marcas","El catalogo no tiene marcas con partes disponibles."):void ze();p.className="nav-grid",p.innerHTML=e.data.map((function(e){return'"})).join(""),p.querySelectorAll(".nav-card").forEach((function(e){e.addEventListener("click",(function(){j.brand={id:parseInt(this.dataset.brandId),name:this.dataset.name},be()}))}))}(JSON.parse(t));de(e+"/brands?mode="+q).then((function(e){if(ue(),e&&e.data&&sessionStorage.setItem(a,JSON.stringify(e)),!e||!e.data||!e.data.length)return e?void pe("Sin marcas","El catalogo no tiene marcas con partes disponibles."):void ze();p.className="nav-grid",p.innerHTML=e.data.map((function(e){return'"})).join(""),p.querySelectorAll(".nav-card").forEach((function(e){e.addEventListener("click",(function(){j.brand={id:parseInt(this.dataset.brandId),name:this.dataset.name},be()}))}))}))}function be(){j.level="models",le(),ge(),r.textContent="Modelos de "+j.brand.name,_e(!0),ce(),de(e+"/models?brand_id="+j.brand.id).then((function(e){ue(),e&&e.data&&e.data.length?(p.className="nav-grid",p.innerHTML=e.data.map((function(e){return'"})).join(""),p.querySelectorAll(".nav-card").forEach((function(e){e.addEventListener("click",(function(){j.model={id:parseInt(this.dataset.modelId),name:this.dataset.name},xe()}))}))):pe("Sin modelos","No hay modelos con partes para "+j.brand.name)}))}function xe(){j.level="years",le(),ge(),r.textContent=j.brand.name+" "+j.model.name+" — Anios",_e(!1),ce(),de(e+"/years?model_id="+j.model.id).then((function(e){ue(),e&&e.data&&e.data.length?(p.className="nav-grid nav-grid--years",p.innerHTML=e.data.map((function(e){return'"})).join(""),p.querySelectorAll(".nav-card").forEach((function(e){e.addEventListener("click",(function(){j.year={id:parseInt(this.dataset.yearId),year:parseInt(this.dataset.year)},Ee()}))}))):pe("Sin anios","No hay anios con partes para este modelo.")}))}function Ee(){j.level="engines",le(),ge(),r.textContent=j.brand.name+" "+j.model.name+" "+j.year.year+" — Motor",_e(!1),ce(),de(e+"/engines?model_id="+j.model.id+"&year_id="+j.year.id).then((function(e){if(ue(),e&&e.data&&e.data.length){if(1===e.data.length){var a=e.data[0];return j.engine={id_mye:a.id_mye,name:a.name_engine+(a.trim_level?" "+a.trim_level:"")},void Te()}p.className="nav-grid",p.innerHTML=e.data.map((function(e){var a=e.name_engine+(e.trim_level?" — "+e.trim_level:"");return'"})).join(""),p.querySelectorAll(".nav-card").forEach((function(e){e.addEventListener("click",(function(){j.engine={id_mye:parseInt(this.dataset.myeId),name:this.dataset.name},Te()}))}))}else pe("Sin motores","No hay configuraciones de motor para esta combinacion.")}))}function ke(){j.level="groups",le(),ge(),r.textContent=j.category.name,_e(!0),ce(),de(e+"/groups?mye_id="+j.engine.id_mye+"&category_id="+j.category.id).then((function(e){ue(),e&&e.data&&e.data.length?(p.className="nav-grid",p.innerHTML=e.data.map((function(e){return'"})).join(""),p.querySelectorAll(".nav-card").forEach((function(e){e.addEventListener("click",(function(){j.group={id:parseInt(this.dataset.groupId),name:this.dataset.name},j.partType=null,Ie()}))}))):pe("Sin subcategorias","No hay subcategorias para "+j.category.name)}))}function Ie(){j.level="part_types",j.partType=null,le(),ge(),r.textContent=j.group.name,_e(!0),ce(),de(e+"/part-types?mye_id="+j.engine.id_mye+"&group_id="+j.group.id).then((function(e){if(ue(),e&&e.data&&e.data.length){if(1===e.data.length){var a=e.data[0];return j.partType={slug:a.slug,name:a.name},void Me(1)}p.className="nav-grid",p.innerHTML=e.data.map((function(e){var a=e.sample_image?'':"";return'"})).join(""),p.querySelectorAll(".nav-card").forEach((function(e){e.addEventListener("click",(function(){j.partType={slug:this.dataset.ptSlug,name:this.dataset.ptName},Me(1)}))}))}else Me(1)}))}function Te(){"local"===q?(j.level="categories",le(),ge(),r.textContent="Categorias (Local)",_e(!0),ce(),de(e+"/categories?mode=local&mye_id="+j.engine.id_mye).then((function(e){ue(),e&&e.data&&e.data.length?(p.className="nav-grid",p.innerHTML=e.data.map((function(e){return'"})).join(""),p.querySelectorAll(".nav-card").forEach((function(e){e.addEventListener("click",(function(){j.nxGroup={slug:this.dataset.slug,name:this.dataset.name},j.nxSubgroup=null,j.nxPartType=null,Se()}))}))):pe("Sin categorias Local","Ninguna parte de este vehiculo mapea al catalogo Local.")}))):(j.level="categories",le(),ge(),r.textContent="Categorias de partes",_e(!0),ce(),de(e+"/categories?mye_id="+j.engine.id_mye).then((function(e){ue(),e&&e.data&&e.data.length?(p.className="nav-grid",p.innerHTML=e.data.map((function(e){return'"})).join(""),p.querySelectorAll(".nav-card").forEach((function(e){e.addEventListener("click",(function(){j.category={id:parseInt(this.dataset.catId),name:this.dataset.name},ke()}))}))):pe("Sin categorias","No hay partes catalogadas para este vehiculo.")})))}function Se(){j.level="groups",le(),ge(),r.textContent=j.nxGroup.name,_e(!0),ce(),de(e+"/groups?mode=local&mye_id="+j.engine.id_mye+"&category_slug="+encodeURIComponent(j.nxGroup.slug)).then((function(e){ue(),e&&e.data&&e.data.length?(p.className="nav-grid",p.innerHTML=e.data.map((function(e){return'"})).join(""),p.querySelectorAll(".nav-card").forEach((function(e){e.addEventListener("click",(function(){j.nxSubgroup={slug:this.dataset.slug,name:this.dataset.name},j.nxPartType=null,Le()}))}))):pe("Sin subcategorias","No hay subcategorias en "+j.nxGroup.name)}))}function Le(){j.level="part_types",j.nxPartType=null,le(),ge(),r.textContent=j.nxSubgroup.name,_e(!0),ce(),de(e+"/part-types?mode=local&mye_id="+j.engine.id_mye+"&group_slug="+encodeURIComponent(j.nxGroup.slug)+"&subgroup_slug="+encodeURIComponent(j.nxSubgroup.slug)).then((function(e){if(ue(),e&&e.data&&e.data.length){if(1===e.data.length){var a=e.data[0];return j.nxPartType={slug:a.slug,name:a.name},void Me(1)}p.className="nav-grid",p.innerHTML=e.data.map((function(e){var a=e.sample_image?'':"";return'"})).join(""),p.querySelectorAll(".nav-card").forEach((function(e){e.addEventListener("click",(function(){j.nxPartType={slug:this.dataset.slug,name:this.dataset.name},Me(1)}))}))}else pe("Sin tipos de parte","No hay tipos de parte en "+j.nxSubgroup.name)}))}function Ce(a){j.level="parts",le(),O=a||1,ge(),r.textContent=j.nxPartType.name,_e(!1),ce(),p.innerHTML="",de(e+"/shop-supplies/parts?group_slug="+encodeURIComponent(j.nxGroup.slug)+"&subgroup_slug="+encodeURIComponent(j.nxSubgroup.slug)+"&part_type_slug="+encodeURIComponent(j.nxPartType.slug)+"&page="+O+"&per_page=30").then((function(e){ue(),e&&e.data&&e.data.length?(m.style.display="",m.innerHTML=e.data.map((function(e){var a;a=e.in_stock_network||e.bodega_count>0?''+e.bodega_count+" bodega"+(e.bodega_count>1?"s":"")+"":'Sin stock';var t=e.image_url?''+ve(e.name)+'':'',n=1===e.priority_tier?" part-card--tier1":2===e.priority_tier?" part-card--tier2":"",o="";if(e.manufacturer){var i=1===e.priority_tier?'':"";o='
'+ve(e.manufacturer)+""+i+"
"}var r=e.part_number?ve(e.part_number)+' · OEM: '+ve(e.oem_part_number||"")+"":ve(e.oem_part_number||"");return'
'+t+'
'+o+'
'+r+'
'+ve(e.name)+'
"})).join(""),m.querySelectorAll(".part-card").forEach((function(e){e.addEventListener("click",(function(){var e=this.dataset.partId;"string"==typeof e&&0===e.indexOf("inv:")||we(parseInt(e))}))})),e.pagination&&Be(e.pagination)):pe("Sin partes","No hay partes en este tipo.")}))}function Me(a){var t;if(j.level="parts",le(),O=a||1,ge(),j.nxPartType?r.textContent=j.nxPartType.name:j.partType?r.textContent=j.partType.name:j.group?r.textContent=j.group.name:r.textContent="Partes",_e(!1),ce(),p.innerHTML="",j.nxGroup&&j.nxSubgroup&&j.nxPartType)t=e+"/parts?mode=local&mye_id="+j.engine.id_mye+"&page="+O+"&per_page=30&nexpart_group="+encodeURIComponent(j.nxGroup.slug)+"&nexpart_subgroup="+encodeURIComponent(j.nxSubgroup.slug)+"&nexpart_part_type="+encodeURIComponent(j.nxPartType.slug);else{var n=j.partType?"&part_type="+encodeURIComponent(j.partType.slug):"";t=e+"/parts?mye_id="+j.engine.id_mye+"&group_id="+j.group.id+"&page="+O+"&per_page=30&mode="+q+n}de(t).then((function(e){if(ue(),e&&e.data&&e.data.length){var a="local"===q;m.style.display="",m.innerHTML=e.data.map((function(e){var t;t=e.local_stock>0?'En stock: '+e.local_stock+"":e.in_stock_network||e.bodega_count>0?''+e.bodega_count+" bodega"+(e.bodega_count>1?"s":"")+"":'Sin stock';var n="local_inventory"===e.source?'Stock Local':"",o=e.image_url?''+ve(e.name)+'':'',i="",r="";if(a&&e.manufacturer){var s="";1===e.priority_tier?(r=" part-card--tier1",s="★"):2===e.priority_tier&&(r=" part-card--tier2",s=""),i='
'+ve(e.manufacturer)+""+(s?''+s+"":"")+"
"}var l=a&&e.part_number?ve(e.part_number)+' · OEM: '+ve(e.oem_part_number)+"":ve(e.oem_part_number);return'
'+o+'
'+i+n+'
'+l+'
'+ve(e.name)+'
"})).join(""),m.querySelectorAll(".part-card").forEach((function(e){e.addEventListener("click",(function(){var e=this.dataset.partId;"string"==typeof e&&0===e.indexOf("inv:")||we(parseInt(e))}))})),e.pagination&&Be(e.pagination)}else pe("Sin partes","No hay partes en este grupo.")}))}function Be(e){if(!e||e.total_pages<=1)v.innerHTML="";else{var a="";e.page<=1?a+='':a+='',function(e,a){if(a<=7){for(var t=[],n=1;n<=a;n++)t.push(n);return t}var o=[1];e>3&&o.push("...");for(var i=Math.max(2,e-1);i<=Math.min(a-1,e+1);i++)o.push(i);e...':t===e.page?a+='":a+='"})),e.page>=e.total_pages?a+='':a+='',v.innerHTML=a,v.querySelectorAll("[data-page]").forEach((function(e){e.addEventListener("click",(function(){g.scrollTo({top:0,behavior:"smooth"}),Me(parseInt(this.dataset.page))}))}))}}function we(a){_.innerHTML='
',h.style.display="none",y.classList.add("is-open"),f.classList.add("is-visible"),U=1,k.textContent="1",de(e+"/part/"+a).then((function(e){if(e&&!e.error){G=e;var a=e.part,t=e.local,n=e.bodegas||[],o=e.alternatives||[],i="";i+='
',a.category_name&&(i+='
'+ve(a.category_name)+" > "+ve(a.group_name)+"
"),i+='
'+ve(a.oem_part_number)+"
",i+='
'+ve(a.name)+"
",a.description&&(i+='
'+ve(a.description)+"
"),a.image_url&&(i+='
'),i+="
",i+='
',i+='
Mi stock
',t&&t.stock>0?(i+='
Cantidad'+t.stock+" "+(t.unit||"PZA")+"
",i+='
Precio publico$'+me(t.price_1)+"
",t.price_2&&(i+='
Precio mayoreo$'+me(t.price_2)+"
"),t.price_3&&(i+='
Precio taller$'+me(t.price_3)+"
"),t.location&&(i+='
Ubicacion'+ve(t.location)+"
")):i+='
No tienes esta parte en inventario.
',i+="
",n.length&&(i+='
',i+='
Disponible en bodegas
',i+='',n.forEach((function(e){i+=""})),i+="
BodegaPrecioStock
"+ve(e.business_name)+""+(e.price?"$"+me(e.price):"--")+""+e.stock+"
"),o.length&&(i+='
',i+='
Alternativas / Cross-references
',o.forEach((function(e){var a=e.local_stock>0?'Stock: '+e.local_stock+"":e.bodega_count>0?''+e.bodega_count+" bod.":"";i+='
'+ve(e.part_number)+'
'+ve(e.manufacturer)+(e.name?" — "+ve(e.name):"")+'
'+a+"
"})),i+="
"),_.innerHTML=i,t&&t.stock>0?h.style.display="":h.style.display="none"}else _.innerHTML='

Error al cargar detalle.

'}))}function Ne(){y.classList.remove("is-open"),f.classList.remove("is-visible"),G=null}function Ae(e){if(!e)return"keyword";var a=e.trim(),t=a.replace(/[\s\-]/g,"").toUpperCase();if(/^[A-HJ-NPR-Z0-9]{17}$/.test(t))return"vin";if(/^[A-Z]{3}[-\s]?\d{3,4}$/.test(a.toUpperCase()))return"plate";if(/[a-z]/.test(a))return"keyword";var n=a.split(/\s+/);if(n.some((function(e){return/^(19|20)\d{2}$/.test(e)}))&&n.length>1)return"keyword";var o=a.toUpperCase();return/^[A-Z0-9]{2,}[\-\/][A-Z0-9]{2,}([\-\/][A-Z0-9]+)*$/.test(o)&&t.length>=6||n.length>=2&&n.every((function(e){return/^[A-Z0-9]{1,}$/.test(e)}))&&t.length>=6||/^[A-Z0-9]{8,}$/.test(t)&&/[A-Z]/.test(t)&&/\d/.test(t)?"part_number":"keyword"}function He(e){var a=J||((J=document.createElement("div")).id="searchHint",J.style.cssText="position:absolute;top:100%;left:0;margin-top:4px;padding:3px 10px;background:var(--color-primary-muted);color:var(--color-text-accent);font-size:var(--text-caption);font-weight:var(--font-weight-semibold);border:1px dashed var(--color-border-accent);border-radius:var(--radius-sm);white-space:nowrap;pointer-events:none;z-index:10;display:none;",o.parentElement.appendChild(J),J),t={vin:"🚗 VIN detectado — decodificando",plate:"🔖 Placa detectada — consultando registro",part_number:"🔩 Numero de parte detectado",keyword:null}[e];t?(a.textContent=t,a.style.display=""):a.style.display="none"}function Pe(e){var a=Ae(e);if("vin"!==a)if("plate"!==a)je(e);else try{!function(e){var a=document.getElementById("plateInput");a?(a.value=e.toUpperCase(),Fe()):je(e)}(e)}catch(a){je(e)}else try{!function(e){var a=document.getElementById("vinInput");a?(a.value=e,Ye()):je(e)}(e)}catch(a){je(e)}}function je(a){de(e+"/search?q="+encodeURIComponent(a)+"&limit=20").then((function(e){if(!e||!e.data||!e.data.length)return i.innerHTML='
Sin resultados para "'+ve(a)+'"
',void i.classList.add("is-visible");i.innerHTML=e.data.map((function(e){var a=e.local_stock>0?'Stock: '+e.local_stock+"":"";return'
'+ve(e.oem_part_number)+'
'+ve(e.name)+"
"+(e.vehicle_info?'
'+ve(e.vehicle_info)+"
":"")+"
"+a+"
"})).join(""),i.classList.add("is-visible"),i.querySelectorAll(".search-result-item").forEach((function(e){e.addEventListener("click",(function(){i.classList.remove("is-visible");var e=this.dataset.partId;"string"==typeof e&&0===e.indexOf("inv:")||we(parseInt(e))}))}))}))}function qe(e,a){a=a||1;var t=$.find((function(a){return a.id===e.id}));t?t.quantity+=a:$.push({id:e.id,part_number:e.part_number,name:e.name,brand:e.brand||"",price:e.price,tax_rate:e.tax_rate||.16,unit:e.unit||"PZA",stock:e.stock,source:e.source||"local",inventory_id:e.inventory_id,quantity:a}),Ue(),Re(),T.classList.contains("open")||$e()}function Oe(e){$.splice(e,1),Ue(),Re()}function Ge(e,a){(a=parseInt(a))<=0?Oe(e):($[e].quantity=a,Ue(),Re())}function Ue(){localStorage.setItem("pos_cart",JSON.stringify($))}function Re(){var e=$.reduce((function(e,a){return e+a.quantity}),0);if(N&&(N.textContent=e,N.style.display=e>0?"flex":"none"),!$.length)return L.innerHTML="",C.style.display="block",A&&(A.disabled=!0),M.textContent="$0.00",B.textContent="$0.00",void(w.textContent="$0.00");C.style.display="none",A&&(A.disabled=!1);var a=0,t=0;L.innerHTML=$.map((function(e,n){var o=e.price*e.quantity,i=o*e.tax_rate;return a+=o,t+=i,'
'+ve(e.name)+'
'+ve(e.part_number)+'
'+e.quantity+'
$'+me(o)+'
'})).join(""),M.textContent="$"+me(a),B.textContent="$"+me(t),w.textContent="$"+me(a+t)}function $e(){var e=T.classList.toggle("open");S.classList.toggle("open",e)}function Ve(){$.length&&(localStorage.setItem("pos_cart",JSON.stringify($)),window.location.href="/pos/sale")}function ze(){document.getElementById("offlineBanner").style.display="",document.getElementById("offlineBannerText").innerHTML="Modo offline — Mostrando solo tu inventario local.",r.textContent="Inventario local",_e(!1),pe("Sin conexion al catalogo","Verifica tu conexion. El catalogo TecDoc requiere acceso al servidor central.")}function Je(){var e=W.value;if(e){var a=F.options[F.selectedIndex].text,t=Y.options[Y.selectedIndex].text,n=Q.options[Q.selectedIndex].text,o=W.options[W.selectedIndex].text;j.brand={id:parseInt(Y.value),name:t},j.model={id:parseInt(Q.value),name:n},j.year={id:parseInt(F.value),year:a},j.engine={id_mye:parseInt(e),name:o},j.level="categories",le(),Te(),setTimeout((function(){var e=document.getElementById("pageBody");e&&e.scrollIntoView({behavior:"smooth",block:"start"})}),300)}}function De(){F.value="",Y.innerHTML='',Q.innerHTML='',W.innerHTML='',Y.disabled=!0,Q.disabled=!0,W.disabled=!0,X.style.display="none",j.level="brands",j.brand=null,j.model=null,j.year=null,j.engine=null,j.category=null,j.group=null,j.partType=null,j.nxGroup=null,j.nxSubgroup=null,j.nxPartType=null,O=1,le(),he()}function Ze(e,a){ae.style.display=e?"":"none",ae.textContent=e,ae.style.color=a?"var(--color-error)":"var(--color-text-muted)"}function Fe(){var a=(ee.value||"").trim().toUpperCase();!a||a.length<5?Ze("Ingresa una placa valida (Ej: ABC-1234).",!0):(Ze("Buscando placa...",!1),de(e+"/plate/"+encodeURIComponent(a)).then((function(e){if(e)if(e.error)Ze(e.error,!0);else{if(!e.found)return ae.style.display="",ae.innerHTML='Placa no registrada. Registrar vehiculo',void(ae.style.color="var(--color-warning, #e6a700)");var a=[];e.year&&a.push(e.year),e.make&&a.push(e.make),e.model&&a.push(e.model);var t=a.join(" ")||"Vehiculo encontrado",n=e.catalog_match;n&&n.brand_id?(Ze(t+" — Cargando catalogo...",!1),Qe(n,e)):Ze(t+" — No encontrado en el catalogo TecDoc.",!1)}else Ze("Error de conexion al buscar placa.",!0)})))}function Ye(){var a=(oe.value||"").trim().toUpperCase();17===a.length?(We("Decodificando VIN...",!1),de(e+"/vin/"+encodeURIComponent(a)).then((function(e){if(e)if(!e.error||e.make){var a=[];e.year&&a.push(e.year),e.make&&a.push(e.make),e.model&&a.push(e.model),e.engine&&a.push(e.engine);var t=a.join(" ")||"Vehiculo no reconocido",n=e.catalog_match;n&&n.brand_id?(We(t+" — Encontrado en catalogo, cargando...",!1),Qe(n,e)):We(t+" — No encontrado en el catalogo TecDoc.",!1)}else We(e.error,!0);else We("Error de conexion al decodificar VIN.",!0)}))):We("El VIN debe tener exactamente 17 caracteres.",!0)}function Qe(a,t){a.year_id&&(F.value=String(a.year_id),de(e+"/brands?year_id="+a.year_id).then((function(t){var n=t&&(t.data||t);n&&(Y.innerHTML=''+n.map((function(e){return'"})).join(""),Y.disabled=!1,X.style.display="",a.brand_id&&(Y.value=String(a.brand_id),de(e+"/models?brand_id="+a.brand_id+"&year_id="+a.year_id).then((function(t){var n=t&&(t.data||t);n&&(Q.innerHTML=''+n.map((function(e){return'"})).join(""),Q.disabled=!1,a.model_id&&(Q.value=String(a.model_id),de(e+"/engines?model_id="+a.model_id+"&year_id="+a.year_id).then((function(e){var t=e&&(e.data||e);t&&(W.innerHTML=''+t.map((function(e){var a=e.name_engine+(e.trim_level?" ("+e.trim_level+")":"");return'"})).join(""),W.disabled=!1,a.id_mye?(W.value=String(a.id_mye),Je(),We("Vehiculo cargado desde VIN.",!1)):1===t.length?(W.value=t[0].id_mye,Je(),We("Vehiculo cargado desde VIN.",!1)):We("Selecciona el motor para continuar.",!1))}))))}))))})))}function We(e,a){ie.style.display=e?"":"none",ie.textContent=e,ie.style.color=a?"var(--color-error)":"var(--color-text-muted)"}}(); \ No newline at end of file diff --git a/pos/static/js/chat.min.js b/pos/static/js/chat.min.js new file mode 100644 index 0000000..04f8242 --- /dev/null +++ b/pos/static/js/chat.min.js @@ -0,0 +1 @@ +!function(){"use strict";let e=!1,t=!1,n=!1,a=null;const s=[],i="webkitSpeechRecognition"in window||"SpeechRecognition"in window;function c(){const e=document.createElement("button");e.className="chat-fab",e.id="chatFab",e.title="Asistente IA",e.innerHTML="💬",e.setAttribute("aria-label","Abrir asistente IA");const t=document.createElement("div");t.className="chat-panel",t.id="chatPanel",t.innerHTML=`\n
\n

Asistente IA — Buscar partes

\n \n
\n
\n
Hola, soy el asistente de Nexus. Dime que refaccion necesitas y te ayudo a encontrarla.
\n
\n \n
\n
\n
\n \n \n \n ${i?'':""}\n \n
\n `,document.body.appendChild(e),document.body.appendChild(t),e.addEventListener("click",l),document.getElementById("chatClose").addEventListener("click",l),document.getElementById("chatSend").addEventListener("click",u),document.getElementById("chatInput").addEventListener("keydown",(function(e){"Enter"!==e.key||e.shiftKey||(e.preventDefault(),u())})),document.getElementById("chatCam").addEventListener("click",(function(){document.getElementById("chatImageInput").click()})),document.getElementById("chatImageInput").addEventListener("change",r),i&&document.getElementById("chatMic").addEventListener("click",o),document.getElementById("chatInput").addEventListener("input",(function(){this.style.height="auto",this.style.height=Math.min(this.scrollHeight,80)+"px"}))}function o(){n?function(){a&&(a.abort(),a=null);n=!1;const e=document.getElementById("chatMic");e&&e.classList.remove("listening")}():function(){const e=window.SpeechRecognition||window.webkitSpeechRecognition;if(!e)return;a=new e,a.lang="es-MX",a.continuous=!1,a.interimResults=!0;const t=document.getElementById("chatInput"),s=document.getElementById("chatMic"),i=t.placeholder;a.onstart=function(){n=!0,s.classList.add("listening"),t.placeholder="Escuchando...",t.value=""},a.onresult=function(e){let n="",a="";for(let t=e.resultIndex;t5242880)return void m("La imagen es muy grande (max 5MB).","ai");const a=new FileReader;a.onload=function(e){const n=document.getElementById("chatMessages"),a=document.getElementById("chatTyping"),i=document.createElement("div");i.className="chat-msg user chat-msg-image",i.innerHTML='Foto de parteIdentificar esta parte',n.insertBefore(i,a),y();var c=e.target.result,o="Identifica esta parte automotriz y sugiere terminos de busqueda.";s.push({role:"user",content:o}),s.length>20&&s.splice(0,2),t=!0,document.getElementById("chatSend").disabled=!0,g(!0);var r=d();fetch("/pos/api/chat",{method:"POST",headers:{"Content-Type":"application/json",Authorization:"Bearer "+r},body:JSON.stringify({message:o,image:c,history:s.slice(-10)})}).then((function(e){return e.json()})).then((function(e){const t=e.response||"No pude identificar la parte. Intenta describirla con texto.";m(t,"ai"),s.push({role:"assistant",content:t}),e.vehicle&&e.vehicle.brand_id&&p(e.vehicle),e.search_results&&e.search_results.length>0&&h(e.search_results)})).catch((function(e){m("Error al procesar imagen: "+e.message,"ai")})).finally((function(){t=!1,document.getElementById("chatSend").disabled=!1,g(!1)}))},a.readAsDataURL(n)}function l(){e=!e;const t=document.getElementById("chatPanel"),n=document.getElementById("chatFab");e?(t.classList.add("open"),n.style.display="none",document.getElementById("chatInput").focus()):(t.classList.remove("open"),n.style.display="flex")}function d(){return window.__pos&&window.__pos.token?window.__pos.token:localStorage.getItem("pos_token")||""}async function u(){if(t)return;const e=document.getElementById("chatInput"),n=e.value.trim();if(n){e.value="",e.style.height="auto",m(n,"user"),s.push({role:"user",content:n}),s.length>20&&s.splice(0,2),t=!0,document.getElementById("chatSend").disabled=!0,g(!0);try{const e=d(),t=await fetch("/pos/api/chat",{method:"POST",headers:{"Content-Type":"application/json",Authorization:"Bearer "+e},body:JSON.stringify({message:n,history:s.slice(-10)})}),a=await t.json();if(!t.ok)return void m("Error: "+(a.error||t.statusText),"ai");const i=a.response||"Sin respuesta.";m(i,"ai"),s.push({role:"assistant",content:i}),a.vehicle&&a.vehicle.brand_id&&p(a.vehicle),a.search_results&&a.search_results.length>0&&h(a.search_results)}catch(e){m("Error de conexion: "+e.message,"ai")}finally{t=!1,document.getElementById("chatSend").disabled=!1,g(!1)}}}function m(e,t){const n=document.getElementById("chatMessages"),a=document.getElementById("chatTyping"),s=document.createElement("div");s.className="chat-msg "+t,s.textContent=e,n.insertBefore(s,a),y()}function p(e){const t=document.getElementById("chatMessages"),n=document.getElementById("chatTyping"),a=document.createElement("div");a.className="chat-vehicle-banner";let s=""+f(e.brand||"")+" "+f(e.model||"")+"";e.year&&(s+=" "+e.year),e.mye_options&&e.mye_options.length>0&&(s+="
Motorizaciones encontradas:",e.mye_options.forEach((function(e){s+="
• "+f(e.engine),e.trim&&(s+=" ("+f(e.trim)+")")}))),a.innerHTML=s,t.insertBefore(a,n),y()}function h(e){const t=document.getElementById("chatMessages"),n=document.getElementById("chatTyping"),a=document.createElement("div");a.className="chat-parts",e.slice(0,8).forEach((function(e){const t=document.createElement("div");t.className="chat-part-card";const n="local"===e.source,s=e.local_stock||0,i=s>0?"in-stock":"",c=s>0?s+" en stock":"Sin stock local",o=e.name_es||e.name_part||"",r=e.oem_part_number||e.part_number||"",d=e.brand||"",u=e.price_1?"$"+parseFloat(e.price_1).toFixed(2):"",m=n?'MI INVENTARIO':'CATÁLOGO';t.innerHTML='
'+f(r)+m+(u?" — "+u:"")+'
'+f(o)+(d?' ('+f(d)+")":"")+'
'+f(c)+"
",t.addEventListener("click",(function(){e.id_part&&"function"==typeof window.openPartDetail&&(window.openPartDetail(e.id_part),l())})),a.appendChild(t)})),t.insertBefore(a,n),y()}function g(e){const t=document.getElementById("chatTyping");t&&t.classList.toggle("visible",e),e&&y()}function y(){const e=document.getElementById("chatMessages");e&&(e.scrollTop=e.scrollHeight)}function f(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}"loading"===document.readyState?document.addEventListener("DOMContentLoaded",c):c()}(); \ No newline at end of file diff --git a/pos/static/js/config.min.js b/pos/static/js/config.min.js new file mode 100644 index 0000000..514813b --- /dev/null +++ b/pos/static/js/config.min.js @@ -0,0 +1 @@ +const Config=(()=>{const e="/pos/api/config";let t=[];function a(){return localStorage.getItem("pos_token")||""}function n(){return!!a()||(window.location.href="/pos/login",!1)}function r(){return{Authorization:`Bearer ${a()}`,"Content-Type":"application/json"}}function o(e,t){var a=document.createElement("div");a.className="cfg-toast cfg-toast--"+(t||"ok"),a.textContent=e,document.body.appendChild(a),setTimeout((function(){a.remove()}),3e3)}function d(e){var t=document.getElementById(e);t&&(t.style.display="flex")}function c(e){var t=document.getElementById(e);t&&(t.style.display="none")}function i(e){document.documentElement.setAttribute("data-theme",e);try{localStorage.setItem("nexus-theme",e)}catch(e){}document.querySelectorAll(".theme-btn").forEach((function(t){t.classList.toggle("is-active",t.dataset.themeTarget===e)})),document.querySelectorAll(".theme-option").forEach((function(e){e.classList.remove("is-selected")}));var t="industrial"===e?0:1,a=document.querySelectorAll(".theme-option");a[t]&&a[t].classList.add("is-selected")}function l(e){i(e)}function s(){var e=new Date,t=String(e.getHours()).padStart(2,"0"),a=String(e.getMinutes()).padStart(2,"0"),n=document.getElementById("live-clock");n&&(n.textContent=t+":"+a)}window.setTheme=i,window.selectThemeOption=l;var u={owner:"Dueno",admin:"Admin",cashier:"Cajero",warehouse:"Almacenista",accountant:"Contador"},m={owner:"badge--owner",admin:"badge--blue",cashier:"badge--green",warehouse:"badge--yellow",accountant:"badge--purple"};function v(e){if(!e)return"";var t=document.createElement("div");return t.textContent=e,t.innerHTML}async function y(){var a=document.getElementById("branches-grid");if(!a)return[];try{var n=await fetch(e+"/branches",{headers:r()});if(!n.ok)throw new Error("Failed");var o=await n.json();t=o.data||[]}catch(e){return console.error("Config.loadBranches:",e),t=[],a.innerHTML='
Error al cargar sucursales
',t}return function(){var e=document.getElementById("branches-grid");if(!e)return;var a="";t.forEach((function(e,t){var n=e.is_active?''+(0===t?"Principal":"Activa")+"":'Inactiva';a+='
'+v(e.name)+'
'+n+"
"+(e.address?'
'+v(e.address)+"
":"")+(e.phone?'
'+v(e.phone)+"
":"")+"
"})),a+='
Agregar Sucursal
Configura una nueva ubicacion
',e.innerHTML=a}(),function(){var e=document.getElementById("emp-branch");if(!e)return;e.innerHTML='',t.forEach((function(t){if(t.is_active){var a=document.createElement("option");a.value=t.id,a.textContent=t.name,e.appendChild(a)}}))}(),t}async function g(t){var a=await fetch(e+"/branches",{method:"POST",headers:r(),body:JSON.stringify(t)});if(!a.ok){var n=await a.json().catch((function(){return{error:a.statusText}}));throw new Error(n.error||"Save failed")}return a.json()}async function f(){var t=document.getElementById("employees-tbody"),a=document.getElementById("employees-count");if(!t)return[];var n=[];try{var o=await fetch(e+"/employees",{headers:r()});if(!o.ok)throw new Error("Failed");n=(await o.json()).data||[]}catch(e){return console.error("Config.loadEmployees:",e),t.innerHTML='Error al cargar empleados',[]}var d=n.filter((function(e){return e.is_active})).length;if(a&&(a.textContent=d+" empleado"+(1!==d?"s":"")+" activo"+(1!==d?"s":"")),0===n.length)return t.innerHTML='Sin empleados registrados',n;var c="";return n.forEach((function(e){var t,a=function(e){if(!e)return"??";var t=e.trim().split(/\s+/);return t.length>=2?(t[0][0]+t[t.length-1][0]).toUpperCase():t[0].substring(0,2).toUpperCase()}(e.name),n=e.is_active?'Activo':'Inactivo';c+='
'+a+'
'+v(e.name)+"
"+v(e.email||"-")+""+(t=e.role,''+v(u[t]||t)+"")+v(e.branch_name||"Todas")+""+n+""+(e.max_discount_pct||0)+'%'})),t.innerHTML=c,n}async function p(t){var a=document.getElementById("employee-modal"),n=a?a.dataset.editId:null,o=e+"/employees",d="POST";n&&(o=e+"/employees/"+n,d="PUT",delete a.dataset.editId);var c=await fetch(o,{method:d,headers:r(),body:JSON.stringify(t)});if(!c.ok){var i=await c.json().catch((function(){return{error:c.statusText}}));throw new Error(i.error||"Save failed")}return c.json()}async function h(){try{var t=await fetch(e+"/business",{headers:r()});if(!t.ok)return;var a=await t.json();b("biz-razon-social",a.razon_social),b("biz-nombre",a.nombre),b("biz-rfc",a.rfc),b("biz-regimen",a.regimen_fiscal),b("biz-direccion",a.direccion),b("biz-telefono",a.telefono),b("biz-email",a.email)}catch(e){console.error("Config.loadBusiness:",e)}}function b(e,t){var a=document.getElementById(e);a&&(a.value=t||"")}function E(e){var t=document.getElementById(e);return t?t.value.trim():""}async function w(){try{var t=await fetch(e+"/currency",{headers:r()});if(!t.ok)return;var a=await t.json(),n=document.getElementById("cfg-currency"),o=document.getElementById("cfg-exchange-rate");n&&(n.value=a.currency||"MXN"),o&&(o.value=a.exchange_rate||17.5),localStorage.setItem("pos_currency",a.currency||"MXN"),localStorage.setItem("pos_exchange_rate",a.exchange_rate||17.5)}catch(e){console.error("Config.loadCurrency:",e)}}function I(){if(n()){try{var e=localStorage.getItem("nexus-theme");"industrial"!==e&&"modern"!==e||i(e)}catch(e){}s(),setInterval(s,3e4),function(){document.querySelector("#branches-grid");var e=document.getElementById("btn-save-branch");e&&e.addEventListener("click",(async function(){var t=document.getElementById("branch-name").value.trim();if(t){e.disabled=!0,e.textContent="Guardando...";try{await g({name:t,address:document.getElementById("branch-address").value.trim(),phone:document.getElementById("branch-phone").value.trim()}),o("Sucursal creada"),c("modal-branch"),document.getElementById("branch-name").value="",document.getElementById("branch-address").value="",document.getElementById("branch-phone").value="",await y()}catch(e){o(e.message,"error")}finally{e.disabled=!1,e.textContent="Guardar Sucursal"}}else o("Nombre de sucursal requerido","error")}));var t=document.getElementById("btn-new-employee");t&&t.addEventListener("click",(function(){d("modal-employee")}));var a=document.getElementById("btn-save-employee");a&&a.addEventListener("click",(async function(){var e=document.getElementById("emp-name").value.trim(),t=document.getElementById("emp-role").value,n=document.getElementById("emp-pin").value.trim();if(e)if(t)if(n&&4===n.length&&/^\d{4}$/.test(n)){var r=document.getElementById("emp-branch").value;a.disabled=!0,a.textContent="Guardando...";try{await p({name:e,email:document.getElementById("emp-email").value.trim()||null,phone:document.getElementById("emp-phone").value.trim()||null,role:t,pin:n,branch_id:r?parseInt(r,10):null,max_discount_pct:parseFloat(document.getElementById("emp-discount").value)||0}),o("Empleado creado"),c("modal-employee"),document.getElementById("emp-name").value="",document.getElementById("emp-email").value="",document.getElementById("emp-phone").value="",document.getElementById("emp-role").value="",document.getElementById("emp-pin").value="",document.getElementById("emp-branch").value="",document.getElementById("emp-discount").value="0",await f()}catch(e){o(e.message,"error")}finally{a.disabled=!1,a.textContent="Guardar Empleado"}}else o("PIN debe ser 4 digitos","error");else o("Selecciona un rol","error");else o("Nombre requerido","error")})),document.querySelectorAll(".cfg-modal-overlay").forEach((function(e){e.addEventListener("click",(function(t){t.target===e&&(e.style.display="none")}))})),document.addEventListener("keydown",(function(e){"Escape"===e.key&&document.querySelectorAll(".cfg-modal-overlay").forEach((function(e){e.style.display="none"}))}))}();var t=document.getElementById("cfg-kiosk-mode");t&&window.NexusKiosk&&(t.checked=window.NexusKiosk.isEnabled(),t.addEventListener("change",(function(){this.checked?(window.NexusKiosk.enable(),o("Modo Kiosko activado")):(window.NexusKiosk.disable(),o("Modo Kiosko desactivado"))}))),y(),f(),h(),w()}}return document.addEventListener("DOMContentLoaded",I),{init:I,setTheme:i,selectThemeOption:l,loadBranches:y,loadEmployees:f,saveBranch:g,saveEmployee:p,editEmployee:async function(t){if(n())try{var a=await fetch(e+"/employees",{headers:r()});if(!a.ok)throw new Error("Failed to load employees");var c=((await a.json()).data||[]).find((function(e){return e.id===t}));if(!c)return void o("Empleado no encontrado","error");b("new-emp-name",c.name),b("new-emp-email",c.email||"");var i=document.getElementById("new-emp-role");i&&(i.value=c.role||"cashier");var l=document.getElementById("new-emp-branch");l&&(l.value=c.branch_id||""),b("new-emp-discount",c.max_discount_pct||""),b("new-emp-pin","");var s=document.getElementById("employee-modal");if(s){s.dataset.editId=t;var u=s.querySelector(".modal-title, h3");u&&(u.textContent="Editar Empleado")}d("employee-modal")}catch(e){o("Error: "+e.message,"error")}},loadBusiness:h,saveBusiness:async function(){if(n()){var t={razon_social:E("biz-razon-social"),nombre:E("biz-nombre"),rfc:E("biz-rfc"),regimen_fiscal:E("biz-regimen"),direccion:E("biz-direccion"),telefono:E("biz-telefono"),email:E("biz-email")};try{var a=await fetch(e+"/business",{method:"PUT",headers:r(),body:JSON.stringify(t)});if(!a.ok){var d=await a.json().catch((function(){return{error:a.statusText}}));throw new Error(d.error||"Error al guardar")}o("Datos de empresa guardados","ok")}catch(e){o(e.message,"error")}}},saveTaxParams:async function(){if(n()){var t={tax_iva:E("tax-iva")||"16",tax_ieps:E("tax-ieps")||"0",invoice_serie:E("tax-serie")||"FA",invoice_folio:E("tax-folio")||"1",default_currency:document.getElementById("tax-moneda")?document.getElementById("tax-moneda").value:"MXN",default_payment_method:document.getElementById("tax-forma-pago")?document.getElementById("tax-forma-pago").value:"01"};try{if(!(await fetch(e+"/business",{method:"PUT",headers:r(),body:JSON.stringify(t)})).ok)throw new Error("Error al guardar");o("Parámetros de impuestos guardados","ok")}catch(e){o(e.message,"error")}}},loadCurrency:w,saveCurrency:async function(){var t=document.getElementById("cfg-currency"),a=document.getElementById("cfg-exchange-rate"),n=document.getElementById("currency-status"),d=document.getElementById("btn-save-currency");if(t&&a){var c=t.value,i=parseFloat(a.value);if(!i||i<=0)o("Tipo de cambio invalido","error");else{d&&(d.disabled=!0,d.textContent="Guardando...");try{var l=await fetch(e+"/currency",{method:"PUT",headers:r(),body:JSON.stringify({currency:c,exchange_rate:i})});if(!l.ok){var s=await l.json().catch((function(){return{error:l.statusText}}));throw new Error(s.error||"Save failed")}localStorage.setItem("pos_currency",c),localStorage.setItem("pos_exchange_rate",i),o("Moneda actualizada"),n&&(n.textContent=c+" — TC: "+i)}catch(e){o(e.message,"error")}finally{d&&(d.disabled=!1,d.textContent="Guardar Moneda")}}}},openModal:d,closeModal:c}})(); \ No newline at end of file diff --git a/pos/static/js/customers.min.js b/pos/static/js/customers.min.js new file mode 100644 index 0000000..3640222 --- /dev/null +++ b/pos/static/js/customers.min.js @@ -0,0 +1 @@ +const Customers=(()=>{let e=localStorage.getItem("pos_token")||"",t=1,n=1,o=null,a=null;const i=e=>"$"+parseFloat(e||0).toLocaleString("es-MX",{minimumFractionDigits:2,maximumFractionDigits:2});async function l(t,n={}){n.headers={"Content-Type":"application/json",Authorization:"Bearer "+e};const o=await fetch(t,n);if(401===o.status)return void(window.location.href="/pos/login");const a=await o.json();if(!o.ok)throw new Error(a.error||`HTTP ${o.status}`);return a}const d={1:"Mostrador",2:"Taller",3:"Mayoreo"},r={1:"mostrador",2:"taller",3:"mayoreo"};function s(e){return e.credit_balance>0&&e.credit_limit>0&&e.credit_balance>e.credit_limit?'Mora':'Activo'}function c(e){if(!e)return"--";const t=e.trim().split(/\s+/);return t.length>=2?(t[0][0]+t[1][0]).toUpperCase():e.substring(0,2).toUpperCase()}function m(e){if(!e)return"-";try{return new Date(e).toLocaleDateString("es-MX",{day:"2-digit",month:"short",year:"numeric"})}catch(t){return e}}async function p(e,a){e=e||t;const c=document.getElementById("searchInput");a=void 0!==a?a:c&&c.value||"";try{const c=new URLSearchParams({page:e,per_page:50});a&&c.append("q",a);const p=await l(`/pos/api/customers?${c}`);!function(e){const t=document.getElementById("customersBody");if(!t)return;if(t.innerHTML="",!e||0===e.length)return void(t.innerHTML='Sin resultados.');e.forEach(((e,n)=>{const a=d[e.price_tier]||"Mostrador",l=r[e.price_tier]||"mostrador",c=parseFloat(e.credit_limit||0),p=parseFloat(e.credit_balance||0),u=Math.max(0,c-p),v=c>0?Math.round(p/c*100):0,f=v>=80?"none":v>=60?"low":"",y=String(e.id).padStart(5,"0"),b=document.createElement("tr");o&&o.id===e.id&&(b.className="selected"),b.onclick=()=>g(e.id),b.innerHTML=`\n ${y}\n \n
${e.name||""}
\n
${e.email||""}
\n \n ${e.rfc||"-"}\n ${e.phone||"-"}\n ${e.email||"-"}\n ${a}\n ${i(u)}\n ${m(e.last_purchase||e.created_at)}\n ${s(e)}\n `,t.appendChild(b)}))}(p.data||[]),function(e){const o=document.querySelector(".pagination"),a=document.getElementById("tableInfo");if(!e||!e.total_pages)return o&&(o.innerHTML=""),void(a&&(a.textContent=""));n=e.total_pages,t=e.page;const i=e.total||0,l=e.per_page||50,d=(e.page-1)*l+1,r=Math.min(e.page*l,i);a&&(a.textContent=`Mostrando ${d}–${r} de ${i.toLocaleString("es-MX")} clientes`);if(!o)return;if(n<=1)return void(o.innerHTML="");let s="";s+=``;const c=7;let m=Math.max(1,e.page-3),p=Math.min(n,m+c-1);p-m${t}`;p...',s+=``);s+=``,o.innerHTML=s}(p.pagination||{})}catch(e){console.error("Load customers failed:",e)}}function u(){clearTimeout(a),a=setTimeout((()=>{t=1,p(1)}),300)}async function g(e){try{const n=await l(`/pos/api/customers/${e}`);o=n;const a=document.getElementById("detailEmpty"),u=document.getElementById("detailContent");a&&(a.style.display="none"),u&&(u.style.display="flex");const g=document.getElementById("detailAvatar");g&&(g.textContent=c(n.name));const v=document.getElementById("detailName");v&&(v.textContent=(n.name||"").toUpperCase());const f=document.getElementById("detailRFC");f&&(f.textContent=n.rfc||"-");const y=document.getElementById("detailTipo");if(y){const e=d[n.price_tier]||"Mostrador",t=r[n.price_tier]||"mostrador";y.textContent=e,y.className=`tipo-chip tipo-chip--${t}`}const b=document.getElementById("detailStatus");b&&(b.innerHTML=s(n));const h=(e,t)=>{const n=document.getElementById(e);n&&(n.textContent=t||"-")};h("detailAddress",n.address),h("detailPhone",n.phone),h("detailEmail",n.email),h("detailSince",m(n.created_at)),h("detailLastPurchase",m(n.last_purchase));const x=parseFloat(n.credit_limit||0),E=parseFloat(n.credit_balance||0),C=Math.max(0,x-E),I=x>0?Math.round(E/x*100):0;h("detailCreditLimit",i(x)),h("detailCreditAvail",i(C)),h("detailCreditUsed",i(E)),h("detailCreditPct",I+"%");const B=document.getElementById("detailCreditBar");B&&(B.style.width=I+"%",B.className="progress-bar__fill "+(I<40?"low":I>75?"high":""));const M=document.getElementById("historyBody");if(M){const e=n.recent_purchases||[];0===e.length?M.innerHTML='Sin compras recientes':(M.innerHTML="",e.forEach((e=>{const t="paid"===e.status?"mbadge--paid":"overdue"===e.status?"mbadge--overdue":"mbadge--pending",n="paid"===e.status?"Pagado":"overdue"===e.status?"Vencido":"Pendiente",o=document.createElement("tr");o.innerHTML=`\n ${m(e.created_at)}\n NX-${String(e.id).padStart(5,"0")}\n ${i(e.total)}\n ${n}\n `,M.appendChild(o)})))}!function(e){const t=(e,t)=>{const n=document.getElementById(e);n&&(n.textContent=t||"--")};t("panelAvatar",c(e.name)),t("panelName",e.name),t("panelRfc","RFC: "+(e.rfc||"-"));const n=parseFloat(e.credit_limit||0),o=parseFloat(e.credit_balance||0),a=Math.max(0,n-o),l=n>0?Math.round(o/n*100):0;t("panelCreditLimit",i(n)),t("panelCreditUsed",i(o)),t("panelCreditAvail",i(a));const d=document.getElementById("panelCreditBar");d&&(d.style.width=l+"%");const r=document.getElementById("panelVehicles");if(r){const t=e.vehicle_info||[];0===t.length?r.innerHTML='Sin vehiculos registrados':r.innerHTML=t.map((e=>`
\n ${e.make||""} ${e.model||""} ${e.year||""}\n ${e.plates?` Placas: ${e.plates}`:""}\n
`)).join("")}const s=document.getElementById("panelPurchases");if(s){const t=e.recent_purchases||[];0===t.length?s.innerHTML='Sin compras recientes':s.innerHTML=t.slice(0,5).map((e=>`
\n NX-${String(e.id).padStart(5,"0")} — ${m(e.created_at)}\n ${i(e.total)}\n
`)).join("")}}(n),p(t)}catch(e){console.error("Error loading customer:",e)}}function v(){const e=document.getElementById("customerModal");e&&(document.getElementById("modalTitle").textContent="Nuevo Cliente",document.getElementById("editId").value="",document.getElementById("fName").value="",document.getElementById("fRfc").value="",document.getElementById("fRazonSocial").value="",document.getElementById("fRegimenFiscal").value="",document.getElementById("fUsoCfdi").value="G03",document.getElementById("fCp").value="",document.getElementById("fPhone").value="",document.getElementById("fEmail").value="",document.getElementById("fAddress").value="",document.getElementById("fPriceTier").value="1",document.getElementById("fCreditLimit").value="0",e.classList.add("active"),document.getElementById("fName").focus())}function f(){if(!o)return;const e=o,t=document.getElementById("customerModal");t&&(document.getElementById("modalTitle").textContent="Editar Cliente",document.getElementById("editId").value=e.id,document.getElementById("fName").value=e.name||"",document.getElementById("fRfc").value=e.rfc||"",document.getElementById("fRazonSocial").value=e.razon_social||"",document.getElementById("fRegimenFiscal").value=e.regimen_fiscal||"",document.getElementById("fUsoCfdi").value=e.uso_cfdi||"G03",document.getElementById("fCp").value=e.cp||"",document.getElementById("fPhone").value=e.phone||"",document.getElementById("fEmail").value=e.email||"",document.getElementById("fAddress").value=e.address||"",document.getElementById("fPriceTier").value=e.price_tier||"1",document.getElementById("fCreditLimit").value=e.credit_limit||0,t.classList.add("active"))}function y(){const e=document.getElementById("customerModal");e&&e.classList.remove("active")}async function b(){if(!o)return;const e=document.getElementById("statementModal");if(!e)return;const t=document.getElementById("statementName");t&&(t.textContent=o.name);const n=document.getElementById("statementContent");n&&(n.innerHTML='
Cargando...
'),e.classList.add("active");try{const e=await l(`/pos/api/customers/${o.id}/statement`);let t=`
\n Saldo actual: ${i(e.balance)} |\n Limite: ${i(e.customer?e.customer.credit_limit:0)}\n
`;e.entries&&0!==e.entries.length?(t+='',t+='',e.entries.forEach((e=>{t+=`\n \n \n \n \n \n `})),t+="
FechaConceptoCargoAbonoSaldo
${m(e.date)}${e.description||""}${"charge"===e.type?i(e.amount):""}${"payment"===e.type?i(e.amount):""}${i(e.running_balance)}
"):t+='
Sin movimientos
',n&&(n.innerHTML=t)}catch(e){n&&(n.innerHTML=`
Error: ${e.message}
`)}}function h(){const e=document.getElementById("paymentModal");e&&e.classList.remove("active")}function x(e){g(e),"function"==typeof openSlidePanel&&openSlidePanel()}return window.filterCustomers=function(){u()},e?(window.openNewCustomerModal=v,function(){const e=document.querySelectorAll(".quick-actions .action-btn");e.length>=1&&(e[0].onclick=()=>{o&&(window.location.href="/pos/?customer="+o.id)}),e.length>=2&&(e[1].onclick=()=>f()),e.length>=3&&(e[2].onclick=()=>b()),e.length>=4&&(e[3].onclick=()=>{o&&g(o.id)})}(),window.viewCustomer=x,function(){if(!document.getElementById("customerModal")){const e=document.createElement("div");e.innerHTML='\n ',document.body.appendChild(e)}if(!document.getElementById("statementModal")){const e=document.createElement("div");e.innerHTML='\n ',document.body.appendChild(e)}if(!document.getElementById("paymentModal")){const e=document.createElement("div");e.innerHTML='\n ',document.body.appendChild(e)}if(!document.getElementById("modalStyles")){const e=document.createElement("style");e.id="modalStyles",e.textContent="\n .modal-overlay {\n position: fixed; top: 0; left: 0; right: 0; bottom: 0;\n background: rgba(0,0,0,0.5); z-index: 9000;\n display: flex; align-items: center; justify-content: center;\n backdrop-filter: blur(2px);\n }\n .modal-overlay.active { display: flex !important; }\n .modal-box {\n background: var(--color-bg-elevated); border: 1px solid var(--color-border);\n border-radius: var(--radius-lg); width: 90%; max-width: 600px;\n max-height: 85vh; overflow-y: auto;\n box-shadow: var(--shadow-xl);\n }\n .modal-box--wide { max-width: 800px; }\n .modal-header {\n display: flex; align-items: center; justify-content: space-between;\n padding: var(--space-4) var(--space-5);\n border-bottom: 1px solid var(--color-border);\n }\n .modal-header h3 {\n font-family: var(--font-heading); font-size: var(--text-h6);\n font-weight: var(--heading-weight-primary); color: var(--color-text-primary);\n letter-spacing: var(--tracking-wide); text-transform: uppercase;\n }\n .modal-body { padding: var(--space-5); }\n .modal-footer {\n display: flex; justify-content: flex-end; gap: var(--space-3);\n padding: var(--space-4) var(--space-5);\n border-top: 1px solid var(--color-border);\n }\n .form-row { display: flex; gap: var(--space-4); margin-bottom: var(--space-4); }\n .form-row > .form-group { flex: 1; }\n .form-group { margin-bottom: var(--space-4); }\n .form-group label {\n display: block; font-size: var(--text-caption); font-weight: var(--font-weight-semibold);\n color: var(--color-text-muted); text-transform: uppercase; letter-spacing: var(--tracking-wider);\n margin-bottom: var(--space-1);\n }\n .form-input {\n width: 100%; height: 38px; padding: 0 var(--space-3);\n background: var(--color-bg-overlay); border: 1.5px solid var(--color-border);\n border-radius: var(--radius-md); font-family: var(--font-body);\n font-size: var(--text-body-sm); color: var(--color-text-primary); outline: none;\n transition: var(--transition-fast);\n }\n .form-input:focus { border-color: var(--color-border-focus); box-shadow: var(--shadow-focus); }\n select.form-input { cursor: pointer; }\n ",document.head.appendChild(e)}}(),p(),async function(){try{const e=await l("/pos/api/customers?page=1&per_page=1"),t=e.pagination?e.pagination.total:0,n=document.querySelectorAll(".summary-card__value");n.length>=1&&(n[0].textContent=t.toLocaleString("es-MX"))}catch(e){}}()):window.location.href="/pos/login",{search:u,goToPage:function(e){e<1||e>n||(t=e,p(e))},loadCustomers:p,showDetail:x,selectCustomer:g,closeDetail:function(){o=null;const e=document.getElementById("detailEmpty"),n=document.getElementById("detailContent");e&&(e.style.display="flex"),n&&(n.style.display="none"),p(t)},showCreateModal:v,editCurrent:f,closeModal:y,save:async function(){const e=document.getElementById("fName"),t=e?e.value.trim():"";if(!t)return void alert("Nombre es requerido");const n=e=>{const t=document.getElementById(e);return t?t.value.trim():""},a={name:t,rfc:n("fRfc")||null,razon_social:n("fRazonSocial")||null,regimen_fiscal:n("fRegimenFiscal")||null,uso_cfdi:n("fUsoCfdi")||"G03",cp:n("fCp")||null,phone:n("fPhone")||null,email:n("fEmail")||null,address:n("fAddress")||null,price_tier:parseInt(n("fPriceTier"))||1,credit_limit:parseFloat(n("fCreditLimit"))||0},i=n("editId");try{i?await l(`/pos/api/customers/${i}`,{method:"PUT",body:JSON.stringify(a)}):await l("/pos/api/customers",{method:"POST",body:JSON.stringify(a)}),y(),p(),i&&o&&g(i)}catch(e){alert("Error: "+e.message)}},showStatement:b,closeStatement:function(){const e=document.getElementById("statementModal");e&&e.classList.remove("active")},showPaymentModal:function(){if(!o)return;const e=document.getElementById("paymentModal");e&&(document.getElementById("paymentCustomerName").textContent=o.name,document.getElementById("paymentAmount").value="",document.getElementById("paymentMethod").value="cash",document.getElementById("paymentRef").value="",e.classList.add("active"),document.getElementById("paymentAmount").focus())},closePayment:h,recordPayment:async function(){if(!o)return;const e=parseFloat(document.getElementById("paymentAmount").value);if(!e||e<=0)return void alert("Ingresa un monto valido");const t=document.getElementById("paymentMethod").value,n=document.getElementById("paymentRef").value.trim();try{await l(`/pos/api/customers/${o.id}/payment`,{method:"POST",body:JSON.stringify({amount:e,method:t,reference:n})}),h(),g(o.id)}catch(e){alert("Error: "+e.message)}}}})(); \ No newline at end of file diff --git a/pos/static/js/dashboard.min.js b/pos/static/js/dashboard.min.js new file mode 100644 index 0000000..971c765 --- /dev/null +++ b/pos/static/js/dashboard.min.js @@ -0,0 +1 @@ +const Dashboard=(()=>{function t(){return localStorage.getItem("pos_token")||""}function e(t){return"$"+parseFloat(t||0).toLocaleString("es-MX",{minimumFractionDigits:2,maximumFractionDigits:2})}function n(t){return parseInt(t||0).toLocaleString("es-MX")}function a(){return(new Date).toISOString().slice(0,10)}function s(t){const e=new Date;return e.setDate(e.getDate()-t),e.toISOString().slice(0,10)}const o=["Dom","Lun","Mar","Mie","Jue","Vie","Sab"],r=["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],i=["Domingo","Lunes","Martes","Miercoles","Jueves","Viernes","Sabado"];function l(t){document.documentElement.setAttribute("data-theme",t);try{localStorage.setItem("nexus-theme",t)}catch(t){}const e=document.getElementById("btn-industrial"),n=document.getElementById("btn-modern");e&&e.classList.toggle("active","industrial"===t),n&&n.classList.toggle("active","modern"===t)}function c(){const t=document.getElementById("sidebar"),e=document.getElementById("sidebar-overlay");t&&t.classList.remove("open"),e&&e.classList.remove("open"),document.body.style.overflow=""}async function d(e){try{const n=await fetch(e,{headers:{Authorization:`Bearer ${t()}`,"Content-Type":"application/json"}});return 401===n.status?(window.location.href="/pos/login",null):n.ok?await n.json():null}catch(t){return console.error("Dashboard fetch error:",e,t),null}}async function m(){const t=a(),s=await d(`/pos/api/register/daily-summary?date=${t}`);if(!s)return u("kpi-ventas-total","kpi-ventas-meta"),u("kpi-tickets-count","kpi-tickets-meta"),u("kpi-promedio-value","kpi-promedio-meta"),null;const o=document.getElementById("kpi-ventas-total"),r=document.getElementById("kpi-ventas-meta");if(o&&(o.textContent=e(s.total_sales)),r){const t=Object.entries(s.sales_by_method||{}).map((([t,n])=>`${p(t)}: ${e(n.amount)}`)).join(" | ");r.innerHTML=t?`${t}`:'Sin ventas aun'}const i=document.getElementById("kpi-tickets-count"),l=document.getElementById("kpi-tickets-meta");if(i&&(i.textContent=n(s.total_sales_count)),l){const t=s.cancelled_count||0;l.innerHTML=t>0?`${t} cancelada${t>1?"s":""}de ${s.total_sales_count+t} totales`:`${s.total_sales_count} completada${1!==s.total_sales_count?"s":""}`}const c=document.getElementById("kpi-promedio-value"),m=document.getElementById("kpi-promedio-meta"),v=s.total_sales_count>0?s.total_sales/s.total_sales_count:0;c&&(c.textContent=e(v)),m&&(m.innerHTML=`sobre ${n(s.total_sales_count)} ventas`);const g=document.getElementById("kpi-cajas-count"),y=document.getElementById("kpi-cajas-meta"),f=s.registers||[],h=f.filter((t=>"open"===t.status)),k=f.filter((t=>"closed"===t.status));return g&&(g.textContent=f.length),y&&(y.innerHTML=`${h.length} abierta${1!==h.length?"s":""}${k.length} cerrada${1!==k.length?"s":""}`),function(t){const n=document.getElementById("registers-list");if(!n)return;if(!t||0===t.length)return void(n.innerHTML='
Sin cajas registradas hoy
');const a=Math.max(...t.map((t=>t.sale_total||0)),1);n.innerHTML=t.map(((t,n)=>{const s="open"===t.status?"kpi-tag--up":"kpi-tag--neutral",o="open"===t.status?"Abierta":"Cerrada",r=a>0?Math.round(t.sale_total/a*100):0;return`\n
\n
${n+1}
\n
\n
Caja ${t.register_number||t.id}
\n
${t.employee_name||"Sin cajero"}  ·  ${t.sale_count||0} ventas  ${o}
\n
\n
\n
\n
\n
${e(t.sale_total)}
\n
`})).join("")}(f),s}function u(t,e){const n=document.getElementById(t),a=document.getElementById(e);n&&(n.textContent="--"),a&&(a.innerHTML='Error al cargar')}function p(t){return t?t.charAt(0).toUpperCase()+t.slice(1):""}function v(t,e,n,a,s){return`\n
\n
${s}
\n
\n
\n ${e}\n ${n}\n
\n
${a}
\n
\n Ver\n
`}function g(t){const e=document.createElement("div");return e.textContent=t||"",e.innerHTML}async function y(){const t=a(),n=await d(`/pos/api/sales?date_from=${t}&date_to=${t}&per_page=10`),s=document.getElementById("recent-sales-tbody");if(!s)return;if(!n||!n.data||0===n.data.length)return void(s.innerHTML='Sin ventas hoy');const o=n.data.slice(0,5),r=await Promise.all(o.map((t=>d(`/pos/api/sales/${t.id}`))));s.innerHTML=o.map(((t,n)=>{const a=r[n],s=t.created_at?t.created_at.slice(11,16):"--:--",o=t.customer_name||"Publico General",i=t.total||0,l=t.payment_method||"efectivo";let c="";a&&a.items&&a.items.length>0&&(c=a.items.slice(0,3).map((t=>`${g(t.name)}${t.quantity>1?" (x"+t.quantity+")":""}`)).join(", "),a.items.length>3&&(c+="..."));const d=function(t){const e=(t||"").toLowerCase();return e.includes("efectivo")||"cash"===e?"efectivo":e.includes("tarjeta")||"card"===e?"tarjeta":e.includes("transferencia")||"transfer"===e?"transferencia":e.includes("credito")||"credit"===e?"credito":"efectivo"}(l),m=p(l),u="cancelled"===t.status?' [CANCELADA]':"";return`\n \n ${s}\n ${g(o)}${u}\n ${c}\n ${e(i)}\n ${m}\n `})).join("")}function f(){if(t()||(window.location.href="/pos/login",0)){try{const t=localStorage.getItem("nexus-theme");"industrial"!==t&&"modern"!==t||l(t)}catch(t){}!function(){const e=new Date,n=e.getHours();let a="Buenos dias";n>=12&&n<19?a="Buenas tardes":n>=19&&(a="Buenas noches");let s="";try{const e=JSON.parse(atob(t().split(".")[1]));s=e.name||e.sub||""}catch(t){}const o=document.getElementById("header-greeting");o&&(o.textContent=s?`${a}, ${s}`:a);const l=document.getElementById("header-subtitle");if(l){const t=i[e.getDay()],n=e.getDate(),a=r[e.getMonth()],s=e.getFullYear();l.textContent=`${t}, ${n} de ${a} de ${s}`}}(),m(),async function(){const t=await d("/pos/api/inventory/alerts"),e=document.getElementById("alerts-grid");if(!e)return;if(!t||!t.data||0===t.data.length)return void(e.innerHTML='\n
\n
\n \n
\n
\n Sin alertas\n
Todo el inventario dentro de parametros normales.
\n
\n
');const n=t.data,a=n.filter((t=>"zero"===t.type)),s=n.filter((t=>"low"===t.type)),o=n.filter((t=>"over"===t.type));let r="";if(a.length>0){const t=a.slice(0,5).map((t=>`${t.name}: ${t.stock} uds`)).join(" · ");r+=v("error","Stock Agotado",`${a.length} items`,t,'')}if(s.length>0){const t=s.slice(0,5).map((t=>`${t.name}: ${t.stock} uds (min ${t.min_stock})`)).join(" · ");r+=v("warning","Stock Bajo",`${s.length} items`,t,'')}if(o.length>0){const t=o.slice(0,5).map((t=>`${t.name}: ${t.stock} uds (max ${t.max_stock})`)).join(" · ");r+=v("orange","Sobrestock",`${o.length} items`,t,'')}e.innerHTML=r||'
Sin alertas
'}(),async function(){const t=a(),n=await d(`/pos/api/sales?date_from=${t}&date_to=${t}&status=completed&per_page=200`),s=document.getElementById("top-products-list");if(!s)return;if(!n||!n.data||0===n.data.length)return void(s.innerHTML='
Sin ventas hoy
');const o=n.data.slice(0,20),r=await Promise.all(o.map((t=>d(`/pos/api/sales/${t.id}`)))),i={};for(const t of r)if(t&&t.items)for(const e of t.items){const t=e.part_number||e.name;i[t]||(i[t]={name:e.name,part_number:e.part_number||"",qty:0,revenue:0}),i[t].qty+=e.quantity||0,i[t].revenue+=e.subtotal||0}const l=Object.values(i).sort(((t,e)=>e.revenue-t.revenue)).slice(0,5);if(0===l.length)return void(s.innerHTML='
Sin productos vendidos
');const c=l[0].revenue||1;s.innerHTML=l.map(((t,n)=>{const a=Math.round(t.revenue/c*100);return`\n
\n
${n+1}
\n
\n
${g(t.name)}
\n
${g(t.part_number)}  ·  ${t.qty} pzas vendidas
\n
\n
\n
\n
\n
${e(t.revenue)}
\n
`})).join("")}(),async function(){const t=document.getElementById("bar-chart"),n=document.getElementById("chart-week-total");if(!t)return;const r=[];for(let t=6;t>=0;t--)r.push(s(t));const i=await Promise.all(r.map((t=>d(`/pos/api/register/daily-summary?date=${t}`))));let l=0;const c=r.map(((t,e)=>{const n=i[e],s=n&&n.total_sales||0;l+=s;const r=new Date(t+"T12:00:00");return{label:o[r.getDay()],total:s,isToday:t===a()}}));n&&(n.innerHTML=`Total semana: ${e(l)}`);const m=Math.max(...c.map((t=>t.total)),1);t.innerHTML=c.map((t=>{const n=Math.max(Math.round(t.total/m*100),2),a=t.isToday?" today":"",s=t.isToday?' style="color:var(--color-primary);font-weight:700;"':"";return`\n
\n
\n
\n
\n ${t.label}\n
`})).join("")}(),y(),setInterval((()=>{m(),y()}),12e4)}}return window.setTheme=l,window.toggleSidebar=function(){const t=document.getElementById("sidebar"),e=document.getElementById("sidebar-overlay");if(!t)return;const n=t.classList.contains("open");t.classList.toggle("open",!n),e&&e.classList.toggle("open",!n),document.body.style.overflow=n?"":"hidden"},window.closeSidebar=c,window.addEventListener("resize",(function(){window.innerWidth>=768&&c()})),window.setPeriod=function(t){t.closest(".period-selector").querySelectorAll(".period-btn").forEach((function(t){t.classList.remove("active")})),t.classList.add("active")},document.addEventListener("DOMContentLoaded",f),{init:f,setTheme:l}})(); \ No newline at end of file diff --git a/pos/static/js/diagrams.min.js b/pos/static/js/diagrams.min.js new file mode 100644 index 0000000..7b1ee1f --- /dev/null +++ b/pos/static/js/diagrams.min.js @@ -0,0 +1 @@ +!function(){"use strict";var e="/pos/api/diagrams",t=localStorage.getItem("pos_token");if(t){var a={Authorization:"Bearer "+t,"Content-Type":"application/json"},n={view:"list",diagrams:[],categories:[],currentDiagram:null,hotspots:[],selectedHotspot:null,scale:1,panX:0,panY:0,isPanning:!1,lastPointer:null},i=document.getElementById("diagramList"),o=document.getElementById("diagramViewer"),r=document.getElementById("svgContainer"),s=document.getElementById("svgWrapper"),c=document.getElementById("backBtn"),l=document.getElementById("zoomInBtn"),d=document.getElementById("zoomOutBtn"),u=document.getElementById("zoomResetBtn"),m=document.getElementById("diagramTitle"),p=document.getElementById("hotspotPanel"),g=document.getElementById("hotspotBody"),v=document.getElementById("hotspotClose"),f=document.getElementById("partsList"),y=document.getElementById("loading"),h=document.getElementById("emptyState"),_=document.getElementById("diagramFilter"),b=document.getElementById("profileAvatar"),E=document.getElementById("profileName"),L=document.getElementById("profileRole");window.DiagramsApp={openDiagram:function(t){k(e+"/"+t,(function(t,r){t||!r||r.error||(n.currentDiagram=r,n.hotspots=r.hotspots||[],function(){n.view="viewer",i.style.display="none",o.style.display="flex",A();var t=n.currentDiagram;m&&(m.textContent=t.name_es||t.name);M(),r=t.id_diagram,s.innerHTML='
Cargando diagrama...
',fetch(e+"/"+r+"/svg",{headers:a}).then((function(e){return e.text()})).then((function(e){s.innerHTML=e;var t=s.querySelector("svg");t&&(t.style.width="100%",t.style.height="100%",t.setAttribute("preserveAspectRatio","xMidYMid meet"),function(e){var t=e.querySelectorAll("[data-hotspot]");t.forEach((function(e){var t=parseInt(e.getAttribute("data-hotspot"));e.style.cursor="pointer",e.addEventListener("click",(function(e){e.stopPropagation(),C(t)})),e.addEventListener("mouseenter",(function(){x(t,!0)})),e.addEventListener("mouseleave",(function(){x(t,!1)}))}))}(t))})).catch((function(){s.innerHTML='
Error al cargar el diagrama
'})),function(){if(!f)return;var e='

Partes en diagrama

';n.hotspots.forEach((function(t){var a=P(t.callout_number),n=t.part_name_es||t.part_name||a.name,i=t.part_number||"";e+='
',e+=''+t.callout_number+"",e+='
',e+='
'+Y(n)+"
",i&&(e+='
'+Y(i)+"
"),e+="
",e+="
"})),0===n.hotspots.length&&(e+='
Sin partes definidas
');f.innerHTML=e}();var r}())}))},selectHotspot:C,viewPart:function(e){window.open("/pos/catalog?part="+e,"_blank")},searchPart:function(e){window.open("/pos/catalog?search="+encodeURIComponent(e),"_blank")},addToCart:function(e){alert("Funcion disponible desde el catalogo. Busca la parte para agregarla al carrito.")}},"loading"===document.readyState?document.addEventListener("DOMContentLoaded",B):B()}else window.location.href="/pos/login";function B(){!function(){try{var e=JSON.parse(localStorage.getItem("pos_user")||"{}");if(E&&(E.textContent=e.name||"--"),L&&(L.textContent=e.role||"--"),b){var t=e.name||"--";b.textContent=t.split(" ").map((function(e){return e[0]||""})).join("").substring(0,2).toUpperCase()}}catch(e){}}(),k(e+"/",(function(e,t){if(!e&&t&&t.data){n.diagrams=t.data;var a={};t.data.forEach((function(e){var t=e.category_name||"Other";a[t]||(a[t]={name:t,name_es:e.category_name_es||t,diagrams:[]}),a[t].diagrams.push(e)})),n.categories=Object.values(a),w()}else h.style.display="flex"})),function(){c&&c.addEventListener("click",I);l&&l.addEventListener("click",(function(){D(1.25)}));d&&d.addEventListener("click",(function(){D(.8)}));u&&u.addEventListener("click",M);v&&v.addEventListener("click",A);_&&_.addEventListener("input",w);r&&(r.addEventListener("wheel",(function(e){e.preventDefault(),D(e.deltaY<0?1.1:.9)}),{passive:!1}),r.addEventListener("mousedown",H),r.addEventListener("mousemove",q),r.addEventListener("mouseup",O),r.addEventListener("mouseleave",O),r.addEventListener("touchstart",(function(e){1===e.touches.length&&H({clientX:e.touches[0].clientX,clientY:e.touches[0].clientY,preventDefault:function(){}})})),r.addEventListener("touchmove",(function(e){1===e.touches.length&&(e.preventDefault(),q({clientX:e.touches[0].clientX,clientY:e.touches[0].clientY}))}),{passive:!1}),r.addEventListener("touchend",O));document.addEventListener("keydown",(function(e){"viewer"===n.view&&("Escape"===e.key&&(n.selectedHotspot?A():I()),"+"!==e.key&&"="!==e.key||D(1.15),"-"===e.key&&D(.87),"0"===e.key&&M())}))}()}function k(e,t){y.style.display="flex",h.style.display="none",fetch(e,{headers:a}).then((function(e){return e.json()})).then((function(e){y.style.display="none",t(null,e)})).catch((function(e){y.style.display="none",t(e)}))}function w(){n.view="list",o.style.display="none",i.style.display="block";var e=(_&&_.value||"").toLowerCase(),t="";n.categories.forEach((function(a){var n=a.diagrams.filter((function(t){return!e||(-1!==(t.name_es||t.name||"").toLowerCase().indexOf(e)||-1!==(t.category_name_es||"").toLowerCase().indexOf(e)||-1!==(t.group_name_es||"").toLowerCase().indexOf(e))}));0!==n.length&&(t+='
',t+='

'+Y(a.name_es||a.name)+"

",t+='
',n.forEach((function(e){t+='
',t+='
',t+=''+Y(e.name_es||e.name)+'',t+="
",t+='
',t+='
'+Y(e.name_es||e.name)+"
",t+='
'+Y(e.group_name_es||e.group_name||"")+"
",t+="
",t+="
"})),t+="
")})),h.style.display=t?"none":"flex",i.innerHTML=t}function I(){n.view="list",n.currentDiagram=null,n.selectedHotspot=null,o.style.display="none",i.style.display="block",A()}function x(e,t){f.querySelectorAll(".part-item").forEach((function(a){parseInt(a.dataset.callout)===e&&a.classList.toggle("is-highlighted",t)}))}function C(e){var t=n.hotspots.find((function(t){return t.callout_number===e}));if(t){n.selectedHotspot=t;var a=s.querySelector("svg");a&&a.querySelectorAll("[data-hotspot]").forEach((function(t){parseInt(t.getAttribute("data-hotspot"))===e?(t.style.fill="rgba(245, 166, 35, 0.25)",t.style.stroke="#F5A623",t.style.strokeWidth="3"):(t.style.fill="transparent",t.style.stroke="none")})),f.querySelectorAll(".part-item").forEach((function(t){t.classList.toggle("is-active",parseInt(t.dataset.callout)===e)})),function(e){p.classList.add("is-open");e.part_name_es||e.part_name||e.callout_number;var t=e.part_number||"",a=e.description_es||e.description||"",n=P(e.callout_number),i=e.part_name_es||e.part_name||n.name,o=a||n.desc,r="";r+='
',r+='
'+e.callout_number+"
",r+='

'+Y(i)+"

",t&&(r+='
No. Parte: '+Y(t)+"
");o&&(r+='

'+Y(o)+"

");r+='
',e.part_id?(r+='',r+=''):r+='";r+="
",r+="
",g.innerHTML=r}(t)}}function P(e){var t=n.currentDiagram;if(!t)return{name:"Parte "+e,desc:""};var a=(t.name||"").toLowerCase();return-1!==a.indexOf("brak")||-1!==a.indexOf("freno")?{1:{name:"Disco de freno",desc:"Disco ventilado de freno delantero. Se recomienda cambiar en pares."},2:{name:"Caliper de freno",desc:"Caliper con pistones, incluye purga. Verificar compatibilidad con tipo de pastilla."},3:{name:"Pastillas de freno",desc:"Juego de pastillas con indicador de desgaste. Material ceramico o semi-metalico."},4:{name:"Manguera de freno",desc:"Manguera flexible de alta presion. Revisar por grietas cada 40,000 km."},5:{name:"Cilindro maestro",desc:"Cilindro maestro con deposito de liquido de frenos. Incluye empaques."}}[e]||{name:"Parte "+e,desc:""}:-1!==a.indexOf("susp")?{1:{name:"Amortiguador",desc:"Amortiguador delantero de gas. Se recomienda cambiar en pares."},2:{name:"Resorte helicoidal",desc:"Resorte de suspension delantera. Verificar altura libre."},3:{name:"Brazo de control",desc:"Brazo inferior de control con bujes. Incluye herraje de montaje."},4:{name:"Rotula",desc:"Rotula inferior de suspension. Incluye guardapolvo y seguros."},5:{name:"Barra de acoplamiento",desc:"Barra de acoplamiento de direccion con terminales. Requiere alineacion."}}[e]||{name:"Parte "+e,desc:""}:(-1!==a.indexOf("engine")||-1!==a.indexOf("motor"))&&{1:{name:"Filtro de aire",desc:"Filtro de aire del motor. Cambiar cada 15,000-20,000 km."},2:{name:"Bujias",desc:"Juego de bujias. Verificar tipo (platino, iridio) segun especificacion del motor."},3:{name:"Banda serpentina",desc:"Banda de accesorios. Revisar tension y desgaste. Incluye alternador, A/C y direccion."},4:{name:"Junta de culata",desc:"Junta de cabeza de cilindros. Material MLS multicapa. Requiere torque especifico."},5:{name:"Filtro de aceite",desc:"Filtro de aceite del motor. Cambiar en cada servicio de aceite."}}[e]||{name:"Parte "+e,desc:""}}function A(){p.classList.remove("is-open"),n.selectedHotspot=null;var e=s.querySelector("svg");e&&e.querySelectorAll("[data-hotspot]").forEach((function(e){e.style.fill="transparent",e.style.stroke="none"})),f&&f.querySelectorAll(".part-item").forEach((function(e){e.classList.remove("is-active")}))}function D(e){n.scale=Math.max(.3,Math.min(5,n.scale*e)),S()}function M(){n.scale=1,n.panX=0,n.panY=0,S()}function S(){s&&(s.style.transform="translate("+n.panX+"px, "+n.panY+"px) scale("+n.scale+")")}function H(e){e.button&&0!==e.button||(n.isPanning=!0,n.lastPointer={x:e.clientX,y:e.clientY})}function q(e){if(n.isPanning&&n.lastPointer){var t=e.clientX-n.lastPointer.x,a=e.clientY-n.lastPointer.y;n.panX+=t,n.panY+=a,n.lastPointer={x:e.clientX,y:e.clientY},S()}}function O(){n.isPanning=!1,n.lastPointer=null}function Y(e){if(!e)return"";var t=document.createElement("div");return t.appendChild(document.createTextNode(e)),t.innerHTML}}(); \ No newline at end of file diff --git a/pos/static/js/fleet.min.js b/pos/static/js/fleet.min.js new file mode 100644 index 0000000..7975c8a --- /dev/null +++ b/pos/static/js/fleet.min.js @@ -0,0 +1 @@ +var Fleet=function(){"use strict";var e="/pos/api/fleet",t=localStorage.getItem("pos_token"),n=[],a=1,o=null;function l(){return{"Content-Type":"application/json",Authorization:"Bearer "+t}}function c(e){return null==e?"0":parseFloat(e).toLocaleString("es-MX")}function i(e){return null==e?"$0.00":"$"+parseFloat(e).toLocaleString("es-MX",{minimumFractionDigits:2,maximumFractionDigits:2})}function d(e){return e?new Date(e).toLocaleDateString("es-MX",{day:"2-digit",month:"short",year:"numeric"}):"—"}function r(e){if(!e)return"";var t=document.createElement("div");return t.textContent=e,t.innerHTML}function u(){fetch(e+"/stats",{headers:l()}).then((function(e){return e.json()})).then((function(e){document.getElementById("statTotal").textContent=c(e.total_vehicles),document.getElementById("statOverdue").textContent=c(e.overdue_count),document.getElementById("statUpcoming").textContent=c(e.upcoming_this_month),document.getElementById("statCost").textContent=i(e.total_cost_month)})).catch((function(){}))}function s(){var t=(document.getElementById("searchInput")||{}).value||"",o=e+"/vehicles?page="+a+"&per_page=50";t&&(o+="&q="+encodeURIComponent(t)),fetch(o,{headers:l()}).then((function(e){return e.json()})).then((function(e){var t;(function(e){var t=document.getElementById("vehicleGrid");if(!e.length)return void(t.innerHTML='
🚚
No hay vehiculos registrados
');var n="";e.forEach((function(e){var t=(e.make||"")+" "+(e.model||"");e.year&&(t+=" "+e.year),n+='
'+r(e.plate||"SIN PLACA")+''+(e.is_active?"Activo":"Inactivo")+'
'+r(t.trim())+"
"+(e.owner_name?"
"+r(e.owner_name)+"
":"")+(e.color?"
Color: "+r(e.color)+"
":"")+'
"})),t.innerHTML=n})(n=e.data||[]),function(e){var t=document.getElementById("vehiclePagination");if(!e||e.total_pages<=1)return void(t.innerHTML="");t.innerHTML='Pagina '+e.page+" de "+e.total_pages+" ("+e.total+' vehiculos)'}(e.pagination),t='',n.forEach((function(e){var n=(e.plate||"S/P")+" - "+(e.make||"")+" "+(e.model||"");t+='"})),["schedVehicleSelect","logVehicleSelect"].forEach((function(e){var n=document.getElementById(e);n&&(n.innerHTML=t)}))})).catch((function(){document.getElementById("vehicleGrid").innerHTML='
Error al cargar vehiculos
'}))}function m(e){var t=document.getElementById("vehicleModal"),n=document.getElementById("vehicleModalTitle");e&&e.id?(n.textContent="Editar Vehiculo",document.getElementById("vehEditId").value=e.id,document.getElementById("vehPlate").value=e.plate||"",document.getElementById("vehVin").value=e.vin||"",document.getElementById("vehMake").value=e.make||"",document.getElementById("vehModel").value=e.model||"",document.getElementById("vehYear").value=e.year||"",document.getElementById("vehMileage").value=e.current_mileage||0,document.getElementById("vehFuel").value=e.fuel_type||"gasolina",document.getElementById("vehColor").value=e.color||"",document.getElementById("vehOwner").value=e.owner_name||"",document.getElementById("vehNotes").value=e.notes||""):(n.textContent="Nuevo Vehiculo",document.getElementById("vehEditId").value="",["vehPlate","vehVin","vehMake","vehModel","vehYear","vehColor","vehOwner","vehNotes"].forEach((function(e){document.getElementById(e).value=""})),document.getElementById("vehMileage").value="0",document.getElementById("vehFuel").value="gasolina"),t.classList.add("is-open")}function v(){document.getElementById("vehicleModal").classList.remove("is-open")}function h(){fetch(e+"/vehicles?per_page=200",{headers:l()}).then((function(e){return e.json()})).then((function(t){var n=(t.data||[]).map((function(t){return fetch(e+"/vehicles/"+t.id+"/schedules",{headers:l()}).then((function(e){return e.json()})).then((function(e){return{vehicle:t,schedules:e.data||[]}}))}));return Promise.all(n)})).then((function(e){!function(e){var t=document.getElementById("maintBody"),n=[];if(e.forEach((function(e){var t=e.vehicle;e.schedules.forEach((function(e){var a=new Date,o=!1;e.next_due_at&&new Date(e.next_due_at)"+r(t.plate||"S/P")+'
'+r((t.make||"")+" "+(t.model||""))+""+r(e.maintenance_type)+''+(l||"—")+""+d(e.last_done_at)+(e.last_done_km?'
'+c(e.last_done_km)+" km":"")+""+(i||"—")+''+(o?"Vencido":"Al dia")+'")}))})),!n.length)return void(t.innerHTML='No hay programas de mantenimiento.
');t.innerHTML=n.join("")}(e)})).catch((function(){document.getElementById("maintBody").innerHTML='Error al cargar'}))}function g(){fetch(e+"/vehicles?per_page=200",{headers:l()}).then((function(e){return e.json()})).then((function(t){var n=(t.data||[]).map((function(t){return fetch(e+"/vehicles/"+t.id,{headers:l()}).then((function(e){return e.json()})).then((function(e){return{vehicle:t,logs:e.recent_logs||[]}}))}));return Promise.all(n)})).then((function(e){!function(e){var t=document.getElementById("historyBody"),n=[];if(e.forEach((function(e){e.logs.forEach((function(t){t._plate=e.vehicle.plate||"S/P",t._make=(e.vehicle.make||"")+" "+(e.vehicle.model||""),n.push(t)}))})),n.sort((function(e,t){return new Date(t.created_at)-new Date(e.created_at)})),!n.length)return void(t.innerHTML='No hay registros de mantenimiento');var a="";n.forEach((function(e){a+=""+d(e.created_at)+""+r(e._plate)+'
'+r(e._make)+""+r(e.maintenance_type)+''+c(e.mileage_at)+''+i(e.cost)+""+r(e.employee_name||"—")+''+r(e.notes||"—")+""})),t.innerHTML=a}(e)})).catch((function(){document.getElementById("historyBody").innerHTML='Error al cargar'}))}function p(){fetch(e+"/alerts",{headers:l()}).then((function(e){return e.json()})).then((function(e){!function(e){var t=document.getElementById("alertsList");if(!e.length)return void(t.innerHTML='
No hay mantenimientos vencidos
');var n="";e.forEach((function(e){var t="";e.next_due_at&&(t+="Vencio: "+d(e.next_due_at)),e.next_due_km&&(t+=(t?" | ":"")+"Limite: "+c(e.next_due_km)+" km (actual: "+c(e.current_mileage)+" km)"),n+='
'+r(e.plate||"S/P")+" — "+r((e.make||"")+" "+(e.model||""))+(e.year?" "+e.year:"")+'
'+r(e.maintenance_type)+"   "+t+'
"})),t.innerHTML=n}(e.data||[])})).catch((function(){document.getElementById("alertsList").innerHTML='
Error al cargar alertas
'}))}function y(){document.getElementById("scheduleModal").classList.remove("is-open")}function f(){var e=document.getElementById("logModal");document.getElementById("logScheduleSelect").innerHTML='',document.getElementById("logType").value="",document.getElementById("logMileage").value="",document.getElementById("logCost").value="",document.getElementById("logParts").value="",document.getElementById("logNotes").value="",e.classList.add("is-open")}function _(){document.getElementById("logModal").classList.remove("is-open")}return document.addEventListener("DOMContentLoaded",(function(){document.querySelectorAll(".tab-btn").forEach((function(e){e.addEventListener("click",(function(){var e=this.getAttribute("data-tab");document.querySelectorAll(".tab-btn").forEach((function(e){e.classList.remove("is-active")})),document.querySelectorAll(".tab-panel").forEach((function(e){e.classList.remove("is-active")})),this.classList.add("is-active"),document.getElementById("tab-"+e).classList.add("is-active"),"maintenance"===e&&h(),"history"===e&&g(),"alerts"===e&&p()}))}));var e=document.getElementById("searchInput");e&&e.addEventListener("input",(function(){clearTimeout(o),o=setTimeout((function(){a=1,s()}),300)})),document.getElementById("btnNewVehicle").addEventListener("click",(function(){m()})),u(),s()})),{openVehicleModal:m,closeVehicleModal:v,saveVehicle:function(){var t=document.getElementById("vehEditId").value,n={plate:document.getElementById("vehPlate").value.trim(),vin:document.getElementById("vehVin").value.trim(),make:document.getElementById("vehMake").value.trim(),model:document.getElementById("vehModel").value.trim(),year:parseInt(document.getElementById("vehYear").value)||null,current_mileage:parseInt(document.getElementById("vehMileage").value)||0,fuel_type:document.getElementById("vehFuel").value,color:document.getElementById("vehColor").value.trim(),owner_name:document.getElementById("vehOwner").value.trim(),notes:document.getElementById("vehNotes").value.trim()};if(n.plate||n.vin){var a=e+"/vehicles",o="POST";t&&(a+="/"+t,o="PUT"),fetch(a,{method:o,headers:l(),body:JSON.stringify(n)}).then((function(e){return e.json()})).then((function(e){e.error?alert(e.error):(v(),s(),u())})).catch((function(e){alert("Error: "+e.message)}))}else alert("Se requiere placa o VIN")},viewVehicle:function(t){fetch(e+"/vehicles/"+t,{headers:l()}).then((function(e){return e.json()})).then((function(e){m(e)}))},goPage:function(e){a=e,s()},openScheduleModal:function(e){var t=document.getElementById("scheduleModal");document.getElementById("schedType").value="",document.getElementById("schedIntervalKm").value="",document.getElementById("schedIntervalMonths").value="",document.getElementById("schedNextDate").value="",document.getElementById("schedNextKm").value="",document.getElementById("schedNotes").value="",e&&(document.getElementById("schedVehicleSelect").value=e),t.classList.add("is-open")},closeScheduleModal:y,saveSchedule:function(){var t=document.getElementById("schedVehicleSelect").value;if(t){var n=document.getElementById("schedType").value.trim();if(n){var a={maintenance_type:n,interval_km:parseInt(document.getElementById("schedIntervalKm").value)||null,interval_months:parseInt(document.getElementById("schedIntervalMonths").value)||null,next_due_at:document.getElementById("schedNextDate").value||null,next_due_km:parseInt(document.getElementById("schedNextKm").value)||null,notes:document.getElementById("schedNotes").value.trim()};fetch(e+"/vehicles/"+t+"/schedules",{method:"POST",headers:l(),body:JSON.stringify(a)}).then((function(e){return e.json()})).then((function(e){e.error?alert(e.error):(y(),h(),u())})).catch((function(e){alert("Error: "+e.message)}))}else alert("Ingrese el tipo de mantenimiento")}else alert("Seleccione un vehiculo")},openLogModal:f,openLogModalFor:function(t,a,o){f(),document.getElementById("logVehicleSelect").value=t,o&&(document.getElementById("logType").value=o),fetch(e+"/vehicles/"+t+"/schedules",{headers:l()}).then((function(e){return e.json()})).then((function(e){var t='';(e.data||[]).forEach((function(e){var n=e.id==a?" selected":"";t+='"})),document.getElementById("logScheduleSelect").innerHTML=t}));var c=n.find((function(e){return e.id==t}));c&&(document.getElementById("logMileage").value=c.current_mileage||"")},closeLogModal:_,saveLog:function(){var t=document.getElementById("logVehicleSelect").value;if(t){var n=document.getElementById("logType").value.trim();if(n){var a={schedule_id:parseInt(document.getElementById("logScheduleSelect").value)||null,maintenance_type:n,mileage_at:parseInt(document.getElementById("logMileage").value)||null,cost:parseFloat(document.getElementById("logCost").value)||0,parts_used:document.getElementById("logParts").value.trim(),notes:document.getElementById("logNotes").value.trim()};fetch(e+"/vehicles/"+t+"/log",{method:"POST",headers:l(),body:JSON.stringify(a)}).then((function(e){return e.json()})).then((function(e){if(e.error)alert(e.error);else{_(),u();var t=document.querySelector(".tab-btn.is-active");if(t){var n=t.getAttribute("data-tab");"maintenance"===n&&h(),"history"===n&&g(),"alerts"===n&&p(),"vehicles"===n&&s()}}})).catch((function(e){alert("Error: "+e.message)}))}else alert("Ingrese el tipo de mantenimiento")}else alert("Seleccione un vehiculo")}}}(); \ No newline at end of file diff --git a/pos/static/js/i18n.min.js b/pos/static/js/i18n.min.js new file mode 100644 index 0000000..783a6a1 --- /dev/null +++ b/pos/static/js/i18n.min.js @@ -0,0 +1 @@ +var I18N={es:{dashboard:"Dashboard",pos:"Punto de Venta",catalog:"Catalogo",inventory:"Inventario",diagrams:"Diagramas",customers:"Clientes",invoicing:"Facturacion",accounting:"Contabilidad",reports:"Reportes",fleet:"Flotillas",whatsapp:"WhatsApp",config:"Configuracion",nav_main:"Principal",nav_management:"Gestion",nav_system:"Sistema",search:"Buscar",save:"Guardar",cancel:"Cancelar",delete:"Eliminar",edit:"Editar",new:"Nuevo",close:"Cerrar",confirm:"Confirmar",back:"Regresar",next:"Siguiente",print:"Imprimir",export:"Exportar",import:"Importar",refresh:"Actualizar",loading:"Cargando...",no_results:"Sin resultados",error:"Error",success:"Exito",warning:"Advertencia",total:"Total",subtotal:"Subtotal",tax:"IVA",price:"Precio",unit_price:"Precio Unitario",cost:"Costo",discount:"Descuento",margin:"Margen",profit:"Utilidad",balance:"Saldo",amount:"Monto",quantity:"Cantidad",stock:"Existencias",min_stock:"Minimo",max_stock:"Maximo",sku:"SKU",barcode:"Codigo de Barras",brand:"Marca",category:"Categoria",description:"Descripcion",location:"Ubicacion",name:"Nombre",date:"Fecha",status:"Estado",actions:"Acciones",id:"ID",type:"Tipo",notes:"Notas",charge:"Cobrar",quote:"Cotizacion",layaway:"Apartado",credit:"Credito",cash:"Efectivo",transfer:"Transferencia",card:"Tarjeta",mixed:"Mixto",change:"Cambio",customer:"Cliente",general_public:"Publico General",sale:"Venta",sales:"Ventas",ticket:"Ticket",receipt:"Recibo",payment:"Pago",payment_method:"Metodo de Pago",add_to_cart:"Agregar",clear_cart:"Limpiar Carrito",hold_sale:"Pausar Venta",recall_sale:"Retomar Venta",cancel_sale:"Cancelar Venta",confirm_payment:"Confirmar Pago",cash_received:"Efectivo Recibido",amount_due:"Total a Pagar",remaining:"Faltante",phone:"Telefono",email:"Correo",address:"Direccion",rfc:"RFC",credit_limit:"Limite de Credito",credit_balance:"Saldo de Credito",price_tier:"Nivel de Precio",invoice:"Factura",cfdi:"CFDI",stamp:"Timbrar",cancel_invoice:"Cancelar Factura",appearance:"Apariencia",business_data:"Datos de la Empresa",employees:"Empleados",printers:"Impresoras",branches:"Sucursales",fiscal_params:"Parametros Fiscales",system_prefs:"Preferencias del Sistema",currency_config:"Moneda",language:"Idioma",theme:"Tema",dark_theme:"Tema oscuro",light_theme:"Tema claro",logout:"Cerrar sesion",currency:"Moneda",exchange_rate:"Tipo de Cambio",default_currency:"Moneda Predeterminada",mxn:"Peso Mexicano",usd:"Dolar Estadounidense",role_owner:"Dueno",role_admin:"Administrador",role_cashier:"Cajero",role_warehouse:"Almacen",role_accountant:"Contador",daily_sales:"Ventas del Dia",weekly_sales:"Ventas de la Semana",monthly_sales:"Ventas del Mes",top_products:"Productos Mas Vendidos",low_stock:"Bajo Stock",vehicle:"Vehiculo",plate:"Placa",vin:"VIN",mileage:"Kilometraje",yes:"Si",no:"No",all:"Todos",active:"Activo",inactive:"Inactivo",pending:"Pendiente",completed:"Completado",cancelled:"Cancelado"},en:{dashboard:"Dashboard",pos:"Point of Sale",catalog:"Catalog",inventory:"Inventory",diagrams:"Diagrams",customers:"Customers",invoicing:"Invoicing",accounting:"Accounting",reports:"Reports",fleet:"Fleet",whatsapp:"WhatsApp",config:"Settings",nav_main:"Main",nav_management:"Management",nav_system:"System",search:"Search",save:"Save",cancel:"Cancel",delete:"Delete",edit:"Edit",new:"New",close:"Close",confirm:"Confirm",back:"Back",next:"Next",print:"Print",export:"Export",import:"Import",refresh:"Refresh",loading:"Loading...",no_results:"No results",error:"Error",success:"Success",warning:"Warning",total:"Total",subtotal:"Subtotal",tax:"Tax",price:"Price",unit_price:"Unit Price",cost:"Cost",discount:"Discount",margin:"Margin",profit:"Profit",balance:"Balance",amount:"Amount",quantity:"Quantity",stock:"Stock",min_stock:"Minimum",max_stock:"Maximum",sku:"SKU",barcode:"Barcode",brand:"Brand",category:"Category",description:"Description",location:"Location",name:"Name",date:"Date",status:"Status",actions:"Actions",id:"ID",type:"Type",notes:"Notes",charge:"Charge",quote:"Quote",layaway:"Layaway",credit:"Credit",cash:"Cash",transfer:"Transfer",card:"Card",mixed:"Mixed",change:"Change",customer:"Customer",general_public:"Walk-in Customer",sale:"Sale",sales:"Sales",ticket:"Ticket",receipt:"Receipt",payment:"Payment",payment_method:"Payment Method",add_to_cart:"Add",clear_cart:"Clear Cart",hold_sale:"Hold Sale",recall_sale:"Recall Sale",cancel_sale:"Cancel Sale",confirm_payment:"Confirm Payment",cash_received:"Cash Received",amount_due:"Amount Due",remaining:"Remaining",phone:"Phone",email:"Email",address:"Address",rfc:"Tax ID (RFC)",credit_limit:"Credit Limit",credit_balance:"Credit Balance",price_tier:"Price Tier",invoice:"Invoice",cfdi:"CFDI",stamp:"Stamp",cancel_invoice:"Cancel Invoice",appearance:"Appearance",business_data:"Business Info",employees:"Employees",printers:"Printers",branches:"Branches",fiscal_params:"Tax Settings",system_prefs:"System Preferences",currency_config:"Currency",language:"Language",theme:"Theme",dark_theme:"Dark theme",light_theme:"Light theme",logout:"Log out",currency:"Currency",exchange_rate:"Exchange Rate",default_currency:"Default Currency",mxn:"Mexican Peso",usd:"US Dollar",role_owner:"Owner",role_admin:"Administrator",role_cashier:"Cashier",role_warehouse:"Warehouse",role_accountant:"Accountant",daily_sales:"Daily Sales",weekly_sales:"Weekly Sales",monthly_sales:"Monthly Sales",top_products:"Top Products",low_stock:"Low Stock",vehicle:"Vehicle",plate:"Plate",vin:"VIN",mileage:"Mileage",yes:"Yes",no:"No",all:"All",active:"Active",inactive:"Inactive",pending:"Pending",completed:"Completed",cancelled:"Cancelled"}},currentLang=localStorage.getItem("pos_lang")||"es";window.t=function(e){return I18N[currentLang]&&I18N[currentLang][e]||I18N.es&&I18N.es[e]||e},window.setLang=function(e){currentLang=e,localStorage.setItem("pos_lang",e),location.reload()},window.getLang=function(){return currentLang}; \ No newline at end of file diff --git a/pos/static/js/inventory.js b/pos/static/js/inventory.js index a269aef..594bb27 100644 --- a/pos/static/js/inventory.js +++ b/pos/static/js/inventory.js @@ -549,7 +549,7 @@ // Product image section html += '
'; if (data.image_url) { - html += '' + esc(data.name) + ''; + html += '' + esc(data.name) + ''; html += '
'; html += ''; html += ''; diff --git a/pos/static/js/inventory.min.js b/pos/static/js/inventory.min.js new file mode 100644 index 0000000..d5d87ea --- /dev/null +++ b/pos/static/js/inventory.min.js @@ -0,0 +1 @@ +!function(){"use strict";var t="/pos/api/inventory",e=localStorage.getItem("pos_token");if(e){var o={Authorization:"Bearer "+e,"Content-Type":"application/json"},n=1,a="",r=null,i=window.switchTab;window.switchTab=function(t){"function"==typeof i&&i(t),"alertas"===t&&v(),"stock"===t&&m(n)};var s,d=document.getElementById("productSearch");d&&d.addEventListener("input",(function(){clearTimeout(s),s=setTimeout((function(){m(1,d.value.trim())}),350)})),window._loadItems=function(t){m(t)},window.loadItems=function(t,e){m(t,e)},window.viewHistory=function(e){c(t+"/items/"+e+"/history").then((function(t){if(t){var e=t.data||[],o="";e.length?(o='',e.forEach((function(t){var e=t.quantity>0?"var(--color-success)":"var(--color-error)";o+='"})),o+="
FechaTipoCantidadCostoEmpleadoNotas
'+u(t.date)+""+u(t.type)+''+(t.quantity>0?"+":"")+t.quantity+''+(t.cost?"$"+l(t.cost):"—")+""+u(t.employee)+''+u(t.notes)+"
"):o='

Sin movimientos

',document.getElementById("historyContent").innerHTML=o,document.getElementById("historyModal").classList.add("is-open")}}))},window.viewProductDetail=g,window.uploadItemImage=function(o){var n=document.createElement("input");n.type="file",n.accept="image/jpeg,image/png,image/webp",n.onchange=function(){if(n.files&&n.files[0]){var a=n.files[0];if(a.size>5242880)alert("Imagen demasiado grande (max 5 MB)");else{var r=new FormData;r.append("file",a);var i=document.getElementById("imgUploadStatus");i&&(i.textContent="Subiendo..."),fetch(t+"/items/"+o+"/image",{method:"POST",headers:{Authorization:"Bearer "+e},body:r}).then((function(t){return t.json()})).then((function(t){t.image_url?g(o):i&&(i.textContent=t.error||"Error")})).catch((function(){i&&(i.textContent="Error de red")}))}}},n.click()},window.deleteItemImage=function(o){confirm("Eliminar imagen de este producto?")&&fetch(t+"/items/"+o+"/image",{method:"DELETE",headers:{Authorization:"Bearer "+e}}).then((function(t){return t.json()})).then((function(t){t.message?g(o):alert(t.error||"Error")})).catch((function(){alert("Error de red")}))},window.closeHistoryModal=function(){document.getElementById("historyModal").classList.remove("is-open")},window.showCreateModal=function(){document.getElementById("createModal").classList.add("is-open");var e=document.getElementById("newPartNumber");e&&!e._classifyBound&&(e._classifyBound=!0,e.addEventListener("blur",(function(){var e=this.value.trim();if(!(e.length<3)){var o,n,a=document.getElementById("newName");if(!a||!a.value.trim())o=e,(n=document.getElementById("createResult")).innerHTML='Consultando IA...',c(t+"/classify/"+encodeURIComponent(o)).then((function(t){if(t){t.name&&(document.getElementById("newName").value=t.name),t.brand&&(document.getElementById("newBrand").value=t.brand);var e=[];t.name&&e.push(t.name),t.brand&&e.push(t.brand),t.vehicle&&e.push(t.vehicle),t.category&&e.push(t.category),e.length>0?n.innerHTML='Sugerido por IA: '+u(e.join(" | "))+"":n.innerHTML='IA no pudo identificar este numero de parte'}})).catch((function(){n.innerHTML=""}))}})))},window.closeCreateModal=function(){document.getElementById("createModal").classList.remove("is-open"),document.getElementById("createResult").innerHTML=""},window.createItem=function(){var e={part_number:document.getElementById("newPartNumber").value.trim(),name:document.getElementById("newName").value.trim(),brand:document.getElementById("newBrand").value.trim(),barcode:document.getElementById("newBarcode").value.trim()||void 0,cost:parseFloat(document.getElementById("newCost").value)||0,price_1:parseFloat(document.getElementById("newPrice1").value)||0,price_2:parseFloat(document.getElementById("newPrice2").value)||0,price_3:parseFloat(document.getElementById("newPrice3").value)||0,min_stock:parseInt(document.getElementById("newMinStock").value)||0,initial_stock:parseInt(document.getElementById("newInitialStock").value)||0,location:document.getElementById("newLocation").value.trim()};e.part_number&&e.name?c(t+"/items",{method:"POST",body:JSON.stringify(e)}).then((function(t){t&&t.id?(document.getElementById("createResult").innerHTML='Creado ID '+t.id+" | Barcode: "+t.barcode+"",m(n)):document.getElementById("createResult").innerHTML=''+(t?t.error||"Error":"Error de red")+""})):document.getElementById("createResult").innerHTML='Numero de parte y nombre son obligatorios'},window.showPurchaseModal=function(){document.getElementById("purchaseModal").classList.add("is-open")},window.closePurchaseModal=function(){document.getElementById("purchaseModal").classList.remove("is-open"),document.getElementById("purchaseResult").innerHTML=""},window.recordPurchase=function(){var e={inventory_id:parseInt(document.getElementById("purchaseItemId").value),quantity:parseInt(document.getElementById("purchaseQty").value),unit_cost:parseFloat(document.getElementById("purchaseCost").value),supplier_invoice:document.getElementById("purchaseInvoice").value.trim(),notes:document.getElementById("purchaseNotes").value.trim()};e.inventory_id&&e.quantity&&e.unit_cost?c(t+"/purchase",{method:"POST",body:JSON.stringify(e)}).then((function(t){document.getElementById("purchaseResult").innerHTML=t&&t.operation_id?'Compra registrada (op #'+t.operation_id+")":''+(t?t.error||"Error":"Error de red")+""})):document.getElementById("purchaseResult").innerHTML='Complete todos los campos obligatorios'},window.showAdjustmentModal=function(){document.getElementById("adjustmentModal").classList.add("is-open")},window.closeAdjustmentModal=function(){document.getElementById("adjustmentModal").classList.remove("is-open"),document.getElementById("adjustResult").innerHTML=""},window.recordAdjustment=function(){var e={inventory_id:parseInt(document.getElementById("adjustItemId").value),quantity:parseInt(document.getElementById("adjustQty").value),reason:document.getElementById("adjustReason").value.trim()};e.inventory_id&&void 0!==e.quantity&&e.reason?c(t+"/adjustment",{method:"POST",body:JSON.stringify(e)}).then((function(t){document.getElementById("adjustResult").innerHTML=t&&t.operation_id?'Ajuste registrado (op #'+t.operation_id+")":''+(t?t.error||"Error":"Error de red")+""})):document.getElementById("adjustResult").innerHTML='Complete todos los campos (razon obligatoria)'},window.showTransferModal=function(){document.getElementById("transferModal").classList.add("is-open")},window.closeTransferModal=function(){document.getElementById("transferModal").classList.remove("is-open"),document.getElementById("transferResult").innerHTML=""},window.recordTransfer=function(){var e={inventory_id:parseInt(document.getElementById("transferItemId").value),from_branch_id:parseInt(document.getElementById("transferFrom").value),to_branch_id:parseInt(document.getElementById("transferTo").value),quantity:parseInt(document.getElementById("transferQty").value),notes:document.getElementById("transferNotes").value.trim()};e.inventory_id&&e.from_branch_id&&e.to_branch_id&&e.quantity?c(t+"/transfer",{method:"POST",body:JSON.stringify(e)}).then((function(t){document.getElementById("transferResult").innerHTML=t&&t.out_operation_id?'Transferencia registrada':''+(t?t.error||"Error":"Error de red")+""})):document.getElementById("transferResult").innerHTML='Complete todos los campos'},window.showCountModal=function(){document.getElementById("countModal").classList.add("is-open"),document.querySelectorAll("#countLines .count-row").length||p()},window.closeCountModal=function(){document.getElementById("countModal").classList.remove("is-open")},window.addCountLine=p,window.startPhysicalCount=function(){var e=document.querySelectorAll("#countLines .count-row"),o=[];e.forEach((function(t){var e=parseInt(t.querySelector(".count-inv-id").value),n=parseInt(t.querySelector(".count-qty").value);e&&!isNaN(n)&&o.push({inventory_id:e,counted_quantity:n})})),o.length?c(t+"/physical-count/start",{method:"POST",body:JSON.stringify({items:o})}).then((function(t){if(t&&t.count_id){r=t.count_id;var e='

Borrador #'+t.count_id+" — "+u(t.message)+"

";e+='',(t.results||[]).forEach((function(t){var o=0===t.difference?"var(--color-success)":t.difference<0?"var(--color-error)":"var(--color-warning)";e+=""})),e+="
IDEsperadoContadoDiferencia
"+t.inventory_id+""+t.expected+""+t.counted+''+(t.difference>0?"+":"")+t.difference+"
",e+='
',e+='',e+='',e+="
",document.getElementById("countResults").innerHTML=e}else document.getElementById("countResults").innerHTML=''+(t?t.error||"Error":"Error de red")+""})):alert("Agregue al menos una linea")},window.approvePhysicalCount=function(){r?c(t+"/physical-count/approve",{method:"POST",body:JSON.stringify({count_id:r})}).then((function(t){t&&"approved"===t.status?(document.getElementById("countResults").innerHTML=''+u(t.message)+"",r=null):document.getElementById("countResults").innerHTML+='
'+(t?t.error||"Error":"Error de red")+""})):alert("No hay borrador activo")},window.cancelDraft=function(){r=null,document.getElementById("countResults").innerHTML='Borrador cancelado'},window.loadAlerts=v,window.printBarcode=function(t,e,o){var n=window.open("","_blank","width=400,height=250");n.document.write("Etiqueta"),n.document.write("

"+t+"

"),n.document.write("

"+e+"

"),n.document.write('

'+o+"

"),n.document.write(""),n.document.close(),n.print()},m(1)}else window.location.href="/pos/login";function c(t,e){return fetch(t,Object.assign({headers:o},e||{})).then((function(t){return 401===t.status?(localStorage.removeItem("pos_token"),window.location.href="/pos/login",null):t.json()}))}function l(t){return(parseFloat(t)||0).toFixed(2)}function u(t){if(!t)return"";var e=document.createElement("div");return e.textContent=t,e.innerHTML}function m(e,o){n=e||1,a=void 0!==o?o:a;var r=new URLSearchParams({page:n,per_page:50});a&&r.set("q",a),c(t+"/items?"+r.toString()).then((function(t){if(t){var e=document.getElementById("productTableBody"),o=t.data||[];if(!o.length)return e.innerHTML='Sin productos',void(document.getElementById("productPagination").innerHTML="");e.innerHTML=o.map((function(t){return''+u(t.barcode)+''+u(t.part_number)+''+u(t.name)+""+u(t.brand)+''+t.stock+'$'+l(t.cost)+'$'+l(t.price_1)+'$'+l(t.price_2)+'$'+l(t.price_3)+""+u(t.location)+' "})).join("");var n=t.pagination||{},a=document.getElementById("productPagination");n.total_pages>1?a.innerHTML='":a.innerHTML=''+(n.total||0)+" productos"}}))}function p(){var t=document.getElementById("countLines"),e=document.createElement("div");e.className="count-row",e.innerHTML='',t.appendChild(e)}function v(){c(t+"/alerts").then((function(t){if(t){var e=t.data||[],o=document.getElementById("alertsContent");if(o)if(e.length){var n="",a=e.filter((function(t){return"critical"===t.severity})),r=e.filter((function(t){return"warning"===t.severity})),i=e.filter((function(t){return"critical"!==t.severity&&"warning"!==t.severity}));a.length&&(n+='
Criticas
'+a.length+"
",n+='
',a.forEach((function(t){var e="zero"===t.type?"AGOTADO":"low"===t.type?"BAJO":t.type.toUpperCase();n+=y(t,e,"critical")})),n+="
"),r.length&&(n+='
Advertencias
'+r.length+"
",n+='
',r.forEach((function(t){n+=y(t,"EXCESO","warning")})),n+="
"),i.length&&(n+='
Informativas
'+i.length+"
",n+='
',i.forEach((function(t){n+=y(t,"INFO","info")})),n+="
"),o.innerHTML=n}else o.innerHTML='

Sin alertas activas

'}}))}function y(t,e,o){return'
['+e+"] "+u(t.part_number)+" — "+u(t.name)+'
Stock: '+t.stock+(t.min_stock?" (min: "+t.min_stock+")":"")+(t.max_stock?" (max: "+t.max_stock+")":"")+" · Sucursal "+t.branch_id+"
"}function g(o){c(t+"/items/"+o).then((function(t){if(t&&!t.error){var n=t.history||[],a="";a+='
',t.image_url?(a+=''+u(t.name)+'',a+='
',a+='',a+='',a+="
"):(a+='
',a+='',a+='
Sin imagen
',a+="
",a+=''),a+='',a+="
",a+='
',a+='
No. Parte'+u(t.part_number)+"
",a+='
Nombre'+u(t.name)+"
",a+='
Marca'+u(t.brand)+"
",a+='
Codigo de Barras'+u(t.barcode)+"
",a+='
Ubicacion'+u(t.location||"-")+"
",a+='
Stock'+(t.stock||0)+"
",a+="
",a+='
',a+='
Costo$'+l(t.cost)+"
",a+='
Precio 1$'+l(t.price_1)+"
",a+='
Precio 2$'+l(t.price_2)+"
",a+='
Precio 3$'+l(t.price_3)+"
",a+="
",a+='
Cross-References / Equivalencias
',a+='
',a+='

Cargando equivalencias...

',a+="
";var r,i=t.part_number,s=t.catalog_part_id;r=s?"/pos/api/catalog/part/"+s:"/pos/api/catalog/search?q="+encodeURIComponent(i),fetch(r,{headers:{Authorization:"Bearer "+e}}).then((function(t){return t.json()})).then((function(t){var o=document.getElementById("crossRefContent");if(o){var n=t.alternatives||[],a=t.bodegas||[];!s&&t.data&&t.data.length>0?fetch("/pos/api/catalog/part/"+t.data[0].id_part,{headers:{Authorization:"Bearer "+e}}).then((function(t){return t.json()})).then((function(t){d(o,t.alternatives||[],t.bodegas||[])})).catch((function(){o.innerHTML='

Sin conexion al catalogo.

'})):d(o,n,a)}})).catch((function(){var t=document.getElementById("crossRefContent");t&&(t.innerHTML='

Sin conexion al catalogo central.

')})),a+='
Vehiculos Compatibles
',a+='
',a+='

Cargando compatibilidades...

',a+="
",fetch("/pos/api/inventory/items/"+o+"/vehicles",{headers:{Authorization:"Bearer "+e}}).then((function(t){return t.json()})).then((function(t){var e=document.getElementById("compatContent");if(e){var n=t.vehicles||[],a="";n.length>0?(a+='',n.forEach((function(t){a+="",a+=''})),a+="
MarcaModeloAnoMotorOrigen
"+u(t.brand||"")+""+u(t.model||"")+""+u(t.year||"")+""+u(t.engine||"")+""+u(t.source||"")+"
"):a+='

Sin vehiculos vinculados.

',a+='
Busca en catalogo central y vincula automaticamente
',e.innerHTML=a}})).catch((function(){var t=document.getElementById("compatContent");t&&(t.innerHTML='

Error al cargar compatibilidades.

')})),a+='
Historial de Movimientos
',n.length?(a+='',n.forEach((function(t){var e=t.quantity>0?"var(--color-success)":"var(--color-error)";a+='"})),a+="
FechaTipoCantidadCostoEmpleadoNotas
'+u(t.date)+""+u(t.type)+''+(t.quantity>0?"+":"")+t.quantity+''+(t.cost?"$"+l(t.cost):"—")+""+u(t.employee)+''+u(t.notes)+"
"):a+='

Sin movimientos

',document.getElementById("historyContent").innerHTML=a,document.getElementById("historyModal").classList.add("is-open")}else alert(t?t.error:"Error de red");function d(t,e,o){var n="";o&&o.length>0&&(n+='
Disponible en Bodegas:
',n+='',o.forEach((function(t){n+=""})),n+="
BodegaStockPrecioUbicacion
"+u(t.business_name||t.bodega||"")+""+(t.stock||t.stock_quantity||0)+'$'+l(t.price||0)+""+u(t.location||t.warehouse_location||"")+"
"),e&&e.length>0&&(n+='
Partes Equivalentes (Aftermarket):
',n+='',e.forEach((function(t){n+='"})),n+="
No. ParteFabricanteNombre
'+u(t.part_number||t.cross_reference_number||"")+""+u(t.manufacturer||t.source_ref||"")+""+u(t.name||t.name_aftermarket_parts||"")+"
"),n||(n='

No se encontraron equivalencias para esta parte.

'),t.innerHTML=n}}))}}(); \ No newline at end of file diff --git a/pos/static/js/invoicing.min.js b/pos/static/js/invoicing.min.js new file mode 100644 index 0000000..e11d111 --- /dev/null +++ b/pos/static/js/invoicing.min.js @@ -0,0 +1 @@ +const Invoicing=(()=>{const t="/pos/api/invoicing";function e(){return localStorage.getItem("pos_token")||""}async function a(a,n={}){const o=await fetch(`${t}${a}`,{headers:{Authorization:`Bearer ${e()}`,"Content-Type":"application/json"},...n});if(!o.ok){const t=await o.json().catch((()=>({error:o.statusText})));throw new Error(t.error||"Request failed")}return o.json()}function n(t){return parseFloat(t||0).toLocaleString("es-MX",{minimumFractionDigits:2,maximumFractionDigits:2})}function o(t){document.querySelectorAll(".tab-btn").forEach((t=>{t.classList.remove("is-active"),t.setAttribute("aria-selected","false")})),document.querySelectorAll(".tab-panel").forEach((t=>t.classList.remove("is-active")));const e=document.querySelector(`.tab-btn[onclick*="'${t}'"]`)||document.getElementById(`tab-${t}`);e&&(e.classList.add("is-active"),e.setAttribute("aria-selected","true"));const a=document.getElementById(`panel-${t}`);a&&a.classList.add("is-active"),"facturas"===t&&r(),"notas"===t&&s(),"complementos"===t&&l(),"cancelaciones"===t&&i()}function c(t){const e={pending:{css:"badge--pendiente",label:"Pendiente"},pendiente:{css:"badge--pendiente",label:"Pendiente"},sending:{css:"badge--proceso",label:"Enviando"},stamped:{css:"badge--timbrada",label:"Timbrada"},timbrada:{css:"badge--timbrada",label:"Timbrada"},failed:{css:"badge--rechazada",label:"Fallido"},cancelled:{css:"badge--cancelada",label:"Cancelada"},cancelada:{css:"badge--cancelada",label:"Cancelada"},ppd:{css:"badge--ppd",label:"PPD"},proceso:{css:"badge--proceso",label:"En proceso"},aceptada:{css:"badge--aceptada",label:"Aceptada SAT"},rechazada:{css:"badge--rechazada",label:"Rechazada SAT"}}[t]||{css:"",label:t||""};return`${e.label}`}async function r(){const e=document.getElementById("panel-facturas");if(!e)return;const o=e.querySelector(".data-table tbody");if(o)try{const r=await a("/queue?per_page=50&type=Ingreso"),s=r.data||[];if(!s.length)return void(o.innerHTML='No hay facturas en este periodo.');o.innerHTML=s.map((e=>`\n ${e.provisional_folio||e.id||"-"}\n ${e.serie||"-"}\n ${e.customer_name||"-"}\n ${e.rfc||"-"}\n $${n(e.subtotal)}\n $${n(e.tax)}\n $${n(e.total)}\n ${e.uso_cfdi||"-"}\n ${c(e.status)}\n \n
\n \n ${e.sale_id?`PDF`:""}\n ${"stamped"===e.status?``:""}\n
\n \n `)).join("");const l=e.querySelector(".table-footer span");l&&(l.textContent=`Mostrando 1–${s.length} de ${r.pagination?.total||s.length} facturas`)}catch(t){o.innerHTML=`Error: ${t.message}`}}async function s(){const e=document.getElementById("panel-notas");if(!e)return;const o=e.querySelector(".data-table tbody");if(o)try{const e=(await a("/queue?per_page=50&type=Egreso")).data||[];if(!e.length)return void(o.innerHTML='No hay notas de credito.');o.innerHTML=e.map((e=>`\n ${e.provisional_folio||"-"}\n ${e.related_folio||"-"}\n ${e.customer_name||"-"}\n ${e.description||"-"}\n $${n(e.total)}\n ${c(e.status)}\n \n
\n \n ${e.sale_id?`PDF`:""}\n
\n \n `)).join("")}catch(t){o.innerHTML=`Error: ${t.message}`}}async function l(){const t=document.getElementById("panel-complementos");if(!t)return;const e=t.querySelector(".data-table tbody");if(e)try{const t=(await a("/queue?per_page=50&type=Pago")).data||[];if(!t.length)return void(e.innerHTML='No hay complementos de pago.');e.innerHTML=t.map((t=>`\n ${t.provisional_folio||"-"}\n ${t.related_folio||"-"}\n ${t.customer_name||"-"}\n $${n(t.total)}\n ${t.payment_method||"-"}\n ${t.created_at?new Date(t.created_at).toLocaleDateString("es-MX"):"-"}\n ${c(t.status)}\n \n
\n \n ${"stamped"===t.status?``:""}\n
\n \n `)).join("")}catch(t){e.innerHTML=`Error: ${t.message}`}}async function i(){const t=document.getElementById("panel-cancelaciones");if(!t)return;const e=t.querySelector(".cancel-grid");if(e)try{const t=(await a("/queue?per_page=50&status=cancelled")).data||[];let o=[];try{o=(await a("/queue?per_page=50&status=cancelling")).data||[]}catch(t){}const r=[...o,...t];if(!r.length)return void(e.innerHTML='

No hay solicitudes de cancelacion.

');e.innerHTML=r.map((t=>{const e="cancelled"===t.status?"cancel-card--aceptada":"cancelling"===t.status?"cancel-card--proceso":!1===t.cancel_accepted?"cancel-card--rechazada":"cancel-card--proceso",a="cancelled"===t.status?c("aceptada"):!1===t.cancel_accepted?c("rechazada"):c("proceso");return`
\n
\n ${t.provisional_folio||`CFDI-${t.id}`}\n ${a}\n
\n
\n
\n Cliente\n ${t.customer_name||"-"}\n
\n
\n RFC\n ${t.rfc||"-"}\n
\n
\n Motivo\n ${t.cancel_motive||"-"}\n
\n
\n Monto\n $${n(t.total)} MXN\n
\n
\n \n
`})).join("")}catch(t){e.innerHTML=`

Error: ${t.message}

`}}function d(t){const e=document.createElement("div");return e.textContent=t,e.innerHTML}let p=null;function u(t){p=t;const e=document.getElementById("modalCancelOverlay");e&&(e.style.display="flex")}async function m(){if(!p)return;const t=document.getElementById("modalCancelOverlay");if(!t)return;const e=t.querySelector('input[name="motivo-sat"]:checked');if(!e)return void alert("Selecciona un motivo de cancelacion.");const n=e.value,o=t.querySelector('input[type="text"]'),c=o?o.value.trim():"";if("01"!==n||c){if(confirm("Confirmar cancelacion ante el SAT?"))try{const e={motive:n};c&&(e.replacement_uuid=c),await a(`/cancel/${p}`,{method:"POST",body:JSON.stringify(e)}),t.style.display="none",p=null,r(),alert("CFDI cancelado exitosamente.")}catch(t){alert("Error: "+t.message)}}else alert("UUID sustituto requerido para motivo 01.")}function v(){const t=document.getElementById("newInvoiceModalOverlay");if(!t)return;const e=t.querySelector("#invoiceSaleId");e&&(e.value=""),document.getElementById("invoiceResult").innerHTML="",t.style.display="flex"}function y(){const t=document.getElementById("newInvoiceModalOverlay");t&&(t.style.display="none")}async function g(){const t=parseInt(document.getElementById("invoiceSaleId").value),e=document.getElementById("invoiceResult");if(t)try{const n=await a("/invoice",{method:"POST",body:JSON.stringify({sale_id:t})});e.innerHTML='Factura generada: '+(n.provisional_folio||"CFDI-"+(n.id||""))+"",r(),setTimeout((()=>y()),1500)}catch(t){e.innerHTML='Error: '+t.message+""}else e.innerHTML='Ingrese un ID de venta valido.'}function f(){alert("Nota de credito: proximamente")}return document.addEventListener("DOMContentLoaded",(function(){(e()||(window.location.href="/pos/login",0))&&(!function(){const t=document.getElementById("live-clock");if(!t)return;const e=()=>{const e=new Date;t.textContent=e.toLocaleTimeString("es-MX",{hour:"2-digit",minute:"2-digit",second:"2-digit"})};e(),setInterval(e,1e3)}(),function(){const t=document.getElementById("modalDetalleOverlay");if(!t)return;const e=t.querySelector('button[onclick*="modalDetalleOverlay"]');e&&(e.onclick=()=>{t.style.display="none"})}(),function(){const t=document.getElementById("modalCancelOverlay");if(!t)return;const e=t.querySelectorAll("div:last-child button");e.length>=2&&(e[e.length-1].onclick=()=>m(),e[e.length-2].onclick=()=>{t.style.display="none",p=null})}(),r())})),window.switchTab=o,window.showNewInvoiceModal=v,window.closeNewInvoiceModal=y,window.submitNewInvoice=g,window.notaCreditoPlaceholder=f,{switchTab:o,loadFacturas:r,loadNotas:s,loadComplementos:l,loadCancelaciones:i,showDetail:async function(t){const e=document.getElementById("modalDetalleOverlay");if(e)try{const o=await a(`/queue/${t}`),c=e.querySelector(".modal-card");if(!c)return;const r=c.querySelector("div > div:first-child > div:first-child"),s=c.querySelector("div > div:first-child > div:nth-child(2)");r&&(r.textContent="Detalle de Factura"),s&&(s.textContent=`${o.provisional_folio||"CFDI-"+o.id} — ${"stamped"===o.status?"Timbrada":"cancelled"===o.status?"Cancelada":"pending"===o.status?"Pendiente":o.status||""}`);const l=c.querySelector("div:nth-child(2)");l&&(l.innerHTML=`\n
\n
\n
Emisor
\n
${o.emisor_name||"Nexus Autoparts SA de CV"}
\n
${o.emisor_rfc||""}
\n
\n
\n
Receptor
\n
${o.customer_name||"-"}
\n
${o.rfc||""}
\n
\n
\n
UUID
\n
${o.uuid_fiscal||"Sin timbrar"}
\n
\n
\n
Total
\n
$${n(o.total)}
\n
\n
\n ${o.error_message?`

Error: ${d(o.error_message)}

`:""}\n ${o.xml_signed||o.xml_unsigned?`\n
Vista previa XML
\n
${d(o.xml_signed||o.xml_unsigned)}
\n `:""}`);const i=c.querySelector("div:last-child button:last-child");i&&"stamped"===o.status?(i.style.display="",i.onclick=()=>{e.style.display="none",u(t)}):i&&(i.style.display="none"),e.dataset.cfdiId=t,e.dataset.saleId=o.sale_id||"",e.style.display="flex"}catch(t){alert("Error al cargar detalle: "+t.message)}},showCancelModal:u,confirmCancel:m,processQueue:async function(){if(confirm("Procesar todos los CFDIs pendientes?"))try{const t=await a("/queue/process",{method:"POST"});alert(`Procesados: ${t.processed}, Timbrados: ${t.stamped}, Fallidos: ${t.failed}`),r()}catch(t){alert("Error: "+t.message)}},showNewInvoiceModal:v,closeNewInvoiceModal:y,submitNewInvoice:g,notaCreditoPlaceholder:f}})(); \ No newline at end of file diff --git a/pos/static/js/kiosk.min.js b/pos/static/js/kiosk.min.js new file mode 100644 index 0000000..d0d02af --- /dev/null +++ b/pos/static/js/kiosk.min.js @@ -0,0 +1 @@ +!function(){"use strict";var e="pos_kiosk_mode";function n(){return window.matchMedia("(display-mode: standalone)").matches||!0===window.navigator.standalone}function t(){return void 0!==window.Capacitor&&window.Capacitor.isNativePlatform&&window.Capacitor.isNativePlatform()}function o(){var o=localStorage.getItem(e);return"true"===o||"false"!==o&&(n()||t())}var a=!1;function i(){if(!a){var e=document.documentElement,n=e.requestFullscreen||e.webkitRequestFullscreen||e.mozRequestFullScreen||e.msRequestFullscreen;n&&(n.call(e).catch((function(){})),a=!0)}}var c=null;async function r(){if("wakeLock"in navigator)try{(c=await navigator.wakeLock.request("screen")).addEventListener("release",(function(){c=null}))}catch(e){}}function u(){window.addEventListener("beforeunload",(function(e){o()&&(e.preventDefault(),e.returnValue="")})),document.addEventListener("contextmenu",(function(e){o()&&e.preventDefault()}));var e=["click","touchstart","keydown"];function n(){o()&&(i(),r()),e.forEach((function(e){document.removeEventListener(e,n)}))}e.forEach((function(e){document.addEventListener(e,n,{once:!1})})),function(){var e=localStorage.getItem("pos_token");if(e&&-1!==window.location.pathname.indexOf("/pos/login"))try{var n=e.split(".");if(3!==n.length)return;var t=JSON.parse(atob(n[1].replace(/-/g,"+").replace(/_/g,"/"))).exp;t&&1e3*t>Date.now()&&(window.location.href="/pos/")}catch(e){}}()}document.addEventListener("visibilitychange",(function(){"visible"===document.visibilityState&&o()&&!c&&r()})),window.NexusKiosk={isEnabled:o,isPWA:n,isCapacitor:t,enable:function(){localStorage.setItem(e,"true"),i(),r()},disable:function(){localStorage.setItem(e,"false"),c&&(c.release(),c=null),document.fullscreenElement&&document.exitFullscreen().catch((function(){}))},toggle:function(){return o()?window.NexusKiosk.disable():window.NexusKiosk.enable(),o()}},o()&&u(),window.addEventListener("storage",(function(n){n.key===e&&"true"===n.newValue&&u()}))}(); \ No newline at end of file diff --git a/pos/static/js/login.min.js b/pos/static/js/login.min.js new file mode 100644 index 0000000..2d990fd --- /dev/null +++ b/pos/static/js/login.min.js @@ -0,0 +1 @@ +!function(){"use strict";var t="",e=document.querySelectorAll("#pinDots .pin-dot"),n=document.getElementById("loginError"),o=new URLSearchParams(window.location.search).get("tenant")||localStorage.getItem("pos_tenant_id"),a=localStorage.getItem("pos_device_id");function i(){e.forEach((function(e,n){e.classList.toggle("filled",n=4||(t+=e,i(),n.textContent="",4===t.length&&submitPin())},window.clearPin=function(){t="",i(),n.textContent=""},window.submitPin=function(){4===t.length&&(n.textContent="",fetch("/pos/api/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tenant_id:parseInt(o),pin:t,device_id:a})}).then((function(t){return t.json().then((function(e){return{ok:t.ok,data:e}}))})).then((function(t){if(!t.ok)return n.textContent=t.data.error||"Error de autenticacion",void clearPin();localStorage.setItem("pos_token",t.data.token),localStorage.setItem("pos_employee",JSON.stringify(t.data.employee)),localStorage.setItem("pos_tenant_id",o),window.location.href="/pos/catalog"})).catch((function(){n.textContent="Error de conexion",clearPin()})))},document.addEventListener("keydown",(function(t){t.key>="0"&&t.key<="9"?addDigit(t.key):"Backspace"===t.key?clearPin():"Enter"===t.key&&submitPin()}));var r=localStorage.getItem("pos_token");r&&o&&(!function(t){try{var e=t.split(".");if(3!==e.length)return!1;var n=e[1].replace(/-/g,"+").replace(/_/g,"/"),o=JSON.parse(atob(n));return!!o.exp&&1e3*o.exp>Date.now()+3e4}catch(t){return!1}}(r)?(localStorage.removeItem("pos_token"),localStorage.removeItem("pos_employee")):window.location.href="/pos/catalog")}(); \ No newline at end of file diff --git a/pos/static/js/native-bridge.min.js b/pos/static/js/native-bridge.min.js new file mode 100644 index 0000000..24f5a76 --- /dev/null +++ b/pos/static/js/native-bridge.min.js @@ -0,0 +1 @@ +!function(){"use strict";window.NexusNative={isNative:"undefined"!=typeof Capacitor,_scanStream:null,_scanVideo:null,async scanBarcode(){if(this.isNative)try{const{Camera:e}=await import("@capacitor/camera");return await e.getPhoto({quality:90,resultType:"base64"})}catch(e){return null}return"BarcodeDetector"in window?new Promise((async e=>{try{const t=await navigator.mediaDevices.getUserMedia({video:{facingMode:"environment",width:{ideal:1280},height:{ideal:720}}});this._scanStream=t;const a=document.createElement("div");a.id="barcode-scan-overlay",a.style.cssText="position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);z-index:99999;display:flex;flex-direction:column;align-items:center;justify-content:center;";const i=document.createElement("video");i.autoplay=!0,i.playsInline=!0,i.style.cssText="width:90%;max-width:500px;border-radius:12px;border:3px solid #F5A623;",i.srcObject=t,this._scanVideo=i;const o=document.createElement("p");o.textContent="Apunta al codigo de barras...",o.style.cssText="color:#fff;font-size:16px;margin-top:16px;font-family:sans-serif;";const s=document.createElement("button");s.textContent="Cancelar",s.style.cssText="margin-top:16px;padding:10px 24px;background:#F5A623;color:#000;border:none;border-radius:6px;font-size:15px;cursor:pointer;font-weight:bold;",s.onclick=()=>{this.stopScan(),a.remove(),e(null)},a.appendChild(i),a.appendChild(o),a.appendChild(s),document.body.appendChild(a);const n=new BarcodeDetector({formats:["ean_13","ean_8","code_128","code_39","qr_code","upc_a","upc_e"]}),r=async()=>{if(this._scanStream){try{const t=await n.detect(i);if(t.length>0){const i=t[0].rawValue;return o.textContent="Codigo detectado: "+i,o.style.color="#4CAF50",void setTimeout((()=>{this.stopScan(),a.remove(),e(i)}),400)}}catch(e){}requestAnimationFrame(r)}};i.onloadedmetadata=()=>{i.play(),requestAnimationFrame(r)}}catch(t){console.error("Camera access error:",t),alert("No se pudo acceder a la camara: "+t.message),e(null)}})):(alert("Tu navegador no soporta escaneo de codigos de barras. Usa Chrome 83+ o un dispositivo movil."),null)},stopScan(){this._scanStream&&(this._scanStream.getTracks().forEach((e=>e.stop())),this._scanStream=null),this._scanVideo=null;var e=document.getElementById("barcode-scan-overlay");e&&e.remove()},async registerPush(){if(!this.isNative)return null;try{const{PushNotifications:e}=await import("@capacitor/push-notifications");"granted"===(await e.requestPermissions()).receive&&await e.register(),e.addListener("registration",(e=>{console.log("Push token:",e.value)})),e.addListener("pushNotificationReceived",(e=>{console.log("Push received:",e)}))}catch(e){console.log("Push not available:",e)}},async vibrate(){if(this.isNative)try{const{Haptics:e,ImpactStyle:t}=await import("@capacitor/haptics");await e.impact({style:t.Light})}catch(e){}},async setupStatusBar(){if(this.isNative)try{const{StatusBar:e,Style:t}=await import("@capacitor/status-bar");await e.setStyle({style:t.Dark}),await e.setBackgroundColor({color:"#0d0d0d"})}catch(e){}}},window.NexusNative.isNative&&(window.NexusNative.setupStatusBar(),window.NexusNative.registerPush())}(); \ No newline at end of file diff --git a/pos/static/js/offline-banner.min.js b/pos/static/js/offline-banner.min.js new file mode 100644 index 0000000..dc2d7fc --- /dev/null +++ b/pos/static/js/offline-banner.min.js @@ -0,0 +1 @@ +!function(){"use strict";var n=document.getElementById("offlineBanner"),e=document.getElementById("offlineBannerText");if(n&&e){var t=null;navigator.onLine||o(),window.addEventListener("offline",o),window.addEventListener("online",(function(){clearTimeout(t),n.className="banner banner--success",n.style.display="flex",n.style.animation="slideDown 0.35s ease-out forwards",e.innerHTML="Conexion restaurada — Sincronizando datos...",t=setTimeout((function(){n.style.animation="slideUp 0.3s ease-in forwards",n.addEventListener("animationend",(function e(){n.style.display="none",n.removeEventListener("animationend",e)}),{once:!0})}),3e3)}))}function o(){clearTimeout(t),n.className="banner banner--error",n.style.display="flex",n.style.animation="slideDown 0.35s ease-out forwards",e.innerHTML="Conexion perdida — Intentando reconectar..."}}(); \ No newline at end of file diff --git a/pos/static/js/onboarding.min.js b/pos/static/js/onboarding.min.js new file mode 100644 index 0000000..ba81a36 --- /dev/null +++ b/pos/static/js/onboarding.min.js @@ -0,0 +1 @@ +!function(){"use strict";if("true"!==localStorage.getItem("pos_onboarding_done")){var e,a,n=0,o={branchRenamed:!1,productCreated:!1,employeeCreated:!1},t=[function(){var e="";try{e=localStorage.getItem("pos_business_name")||""}catch(e){}var a=e||"tu negocio";return i("div",{className:"onb-step-enter"},[i("div",{className:"onb-icon"},["🚀"]),i("h2",{className:"onb-title"},"Bienvenido a Nexus POS"),i("p",{className:"onb-desc"},"Vamos a configurar "+a+" en unos minutos. Este asistente te guiara por los pasos esenciales para empezar a vender.")])},function(){var e=i("div",{className:"onb-step-enter"},[i("div",{className:"onb-icon"},["🏪"]),i("h2",{className:"onb-title"},"Tu Primera Sucursal"),i("p",{className:"onb-desc"},'Ya creamos la sucursal "Principal" para ti. Puedes renombrarla si quieres.')]),a=i("input",{className:"onb-input",type:"text",placeholder:"Principal",value:"Principal",id:"onb-branch-name"}),n=i("div",{className:"onb-error",id:"onb-branch-msg"});return e.appendChild(i("div",{className:"onb-form"},[i("div",{className:"onb-field"},[i("label",{className:"onb-label"},"Nombre de sucursal"),a,n])])),e},function(){var e=i("div",{className:"onb-step-enter"},[i("div",{className:"onb-icon"},["📦"]),i("h2",{className:"onb-title"},"Agrega Tu Primer Producto"),i("p",{className:"onb-desc"},"Registra una pieza para probar el sistema. Despues podras agregar mas desde Inventario.")]);if(o.productCreated)return e.appendChild(i("div",{className:"onb-success"},["✅ ","Producto creado exitosamente."])),e;var a=i("div",{className:"onb-error",id:"onb-product-msg"});return e.appendChild(i("div",{className:"onb-form"},[i("div",{className:"onb-field"},[i("label",{className:"onb-label"},"Numero de parte"),i("input",{className:"onb-input",type:"text",id:"onb-pn",placeholder:"Ej: FIL-ACE-001"})]),i("div",{className:"onb-field"},[i("label",{className:"onb-label"},"Nombre"),i("input",{className:"onb-input",type:"text",id:"onb-pname",placeholder:"Ej: Filtro de aceite"})]),i("div",{className:"onb-field"},[i("label",{className:"onb-label"},"Precio de venta ($)"),i("input",{className:"onb-input",type:"number",id:"onb-pprice",placeholder:"0.00",min:"0",step:"0.01"})]),i("div",{className:"onb-field"},[i("label",{className:"onb-label"},"Stock inicial"),i("input",{className:"onb-input",type:"number",id:"onb-pstock",placeholder:"0",min:"0",step:"1"})]),a])),e},function(){var e=i("div",{className:"onb-step-enter"},[i("div",{className:"onb-icon"},["👤"]),i("h2",{className:"onb-title"},"Crea Tu Primer Empleado"),i("p",{className:"onb-desc"},"Agrega un usuario para el punto de venta. El PIN se usara para iniciar turno.")]);if(o.employeeCreated)return e.appendChild(i("div",{className:"onb-success"},["✅ ","Empleado creado exitosamente."])),e;var a=i("div",{className:"onb-error",id:"onb-emp-msg"});return e.appendChild(i("div",{className:"onb-form"},[i("div",{className:"onb-field"},[i("label",{className:"onb-label"},"Nombre"),i("input",{className:"onb-input",type:"text",id:"onb-ename",placeholder:"Ej: Juan Perez"})]),i("div",{className:"onb-field"},[i("label",{className:"onb-label"},"PIN (4 digitos)"),i("input",{className:"onb-input",type:"password",id:"onb-epin",placeholder:"****",maxlength:"4"})]),i("div",{className:"onb-field"},[i("label",{className:"onb-label"},"Rol"),i("select",{className:"onb-select",id:"onb-erole"},[i("option",{value:"cashier"},"Cajero"),i("option",{value:"warehouse"},"Almacenista"),i("option",{value:"admin"},"Administrador"),i("option",{value:"accountant"},"Contador")])]),a])),e},function(){return i("div",{className:"onb-step-enter"},[i("div",{className:"onb-icon"},["✅"]),i("h2",{className:"onb-title"},"Listo! Tu Sistema Esta Configurado"),i("p",{className:"onb-desc"},"Ya puedes empezar a usar Nexus POS. Aqui tienes accesos rapidos:"),i("div",{className:"onb-links"},[i("a",{className:"onb-link-card",href:"/pos/catalog"},[i("span",{className:"onb-link-icon"},"📖"),"Catalogo"]),i("a",{className:"onb-link-card",href:"/pos/"},[i("span",{className:"onb-link-icon"},"💻"),"Punto de Venta"]),i("a",{className:"onb-link-card",href:"/pos/inventory"},[i("span",{className:"onb-link-icon"},"📦"),"Inventario"])])])}],s=[l,async function(){var e=document.getElementById("onb-branch-name");e&&e.value.trim()&&(o.branchRenamed=!0),l()},async function(){if(o.productCreated)l();else{var e=(document.getElementById("onb-pn")||{}).value||"",a=(document.getElementById("onb-pname")||{}).value||"",n=parseFloat((document.getElementById("onb-pprice")||{}).value)||0,t=parseInt((document.getElementById("onb-pstock")||{}).value)||0,s=document.getElementById("onb-product-msg");if(e.trim()&&a.trim())try{s&&(s.textContent=""),await r("/pos/api/inventory/items",{method:"POST",body:JSON.stringify({part_number:e.trim(),name:a.trim(),price_1:n,initial_stock:t})}),o.productCreated=!0,m(),setTimeout(l,800)}catch(e){s&&(s.textContent=e.message||"Error al crear producto.")}else s&&(s.textContent="Numero de parte y nombre son obligatorios.")}},async function(){if(o.employeeCreated)l();else{var e=(document.getElementById("onb-ename")||{}).value||"",a=(document.getElementById("onb-epin")||{}).value||"",n=(document.getElementById("onb-erole")||{}).value||"cashier",t=document.getElementById("onb-emp-msg");if(e.trim())if(!a||a.length<4)t&&(t.textContent="El PIN debe tener al menos 4 digitos.");else try{t&&(t.textContent=""),await r("/pos/api/config/employees",{method:"POST",body:JSON.stringify({name:e.trim(),pin:a,role:n})}),o.employeeCreated=!0,m(),setTimeout(l,800)}catch(e){t&&(t.textContent=e.message||"Error al crear empleado.")}else t&&(t.textContent="El nombre es obligatorio.")}},function(){c()}];"loading"===document.readyState?document.addEventListener("DOMContentLoaded",p):p()}function i(e,a,n){var o=document.createElement(e);return a&&Object.keys(a).forEach((function(e){"className"===e?o.className=a[e]:0===e.indexOf("on")?o.addEventListener(e.slice(2).toLowerCase(),a[e]):o.setAttribute(e,a[e])})),n&&(Array.isArray(n)||(n=[n]),n.forEach((function(e){"string"==typeof e?o.appendChild(document.createTextNode(e)):e&&o.appendChild(e)}))),o}async function r(e,a){var n=function(){try{return localStorage.getItem("pos_token")||""}catch(e){return""}}(),o={"Content-Type":"application/json"};n&&(o.Authorization="Bearer "+n);var t=await fetch(e,Object.assign({headers:o},a||{})),s=await t.json();if(!t.ok)throw new Error(s.error||"Error "+t.status);return s}function l(){n<4&&(n++,m())}function d(){l()}function c(){localStorage.setItem("pos_onboarding_done","true"),e&&e.parentNode&&(e.style.opacity="0",e.style.transition="opacity var(--duration-normal) var(--ease-in)",setTimeout((function(){e.parentNode.removeChild(e)}),250))}function m(){a.innerHTML="",a.appendChild(t[n]()),function(){var a=e.querySelector(".onboarding-footer");if(!a)return;a.innerHTML="";var t=i("div",{className:"onb-actions"});0===n?t.appendChild(i("span")):n<4?t.appendChild(i("button",{className:"onb-btn onb-btn--ghost",onClick:d},"Saltar")):t.appendChild(i("span"));var r=["Empezar","Siguiente","Guardar Producto","Guardar Empleado","Ir al Sistema"],l=i("button",{className:"onb-btn onb-btn--primary",onClick:s[n]},r[n]);2===n&&o.productCreated&&(l.textContent="Siguiente");3===n&&o.employeeCreated&&(l.textContent="Siguiente");t.appendChild(l),a.appendChild(t);for(var c=i("div",{className:"onb-progress"}),m=0;m<5;m++){var p="onb-dot";m===n?p+=" is-active":m✕',r.appendChild(a),n.forEach((function(t){var o=document.createElement("div"),n=document.createElement("label");n.style.cssText="display:block;font-size:12px;color:var(--color-text-muted);margin-bottom:4px;text-transform:uppercase;letter-spacing:0.05em;",n.textContent=t.label,o.appendChild(n);var a=document.createElement("select");a.style.cssText="width:100%;padding:8px 10px;background:var(--glass-bg,#222);border:1px solid var(--glass-border,#444);border-radius:6px;color:var(--color-text-primary,#fff);font-size:13px;",a.dataset.filterColumn=t.column;var i=document.createElement("option");i.value="",i.textContent=t.allLabel||"Todos",a.appendChild(i),(t.values||[]).forEach((function(t){if(t){var e=document.createElement("option");e.value=t,e.textContent=t,a.appendChild(e)}})),a.addEventListener("change",(function(){e(r)})),o.appendChild(a),r.appendChild(o)}));var i=document.createElement("button");i.style.cssText="padding:8px;background:transparent;border:1px dashed var(--glass-border,#444);border-radius:6px;color:var(--color-text-muted);cursor:pointer;font-size:12px;",i.textContent="Limpiar filtros",i.addEventListener("click",(function(){r.querySelectorAll("select").forEach((function(t){t.value=""})),e(r)})),r.appendChild(i);var l=o.parentElement;l&&(l.style.position="relative"),(l||document.body).appendChild(r),t=r,setTimeout((function(){document.addEventListener("click",(function t(e){r.contains(e.target)||e.target===o||(closeFilterPanel(),document.removeEventListener("click",t))}))}),100)},window.closeFilterPanel=function(){t&&(t.remove(),t=null)},window.getUniqueColumnValues=function(t,e,o){o=o||30;var n={};return t?(t.querySelectorAll("tbody tr").forEach((function(t){var o=t.querySelectorAll("td");if(o[e]){var r=o[e].textContent.trim();r&&"-"!==r&&""!==r&&(n[r]=(n[r]||0)+1)}})),Object.keys(n).sort((function(t,e){return n[e]-n[t]})).slice(0,o)):[]};var o=null;function n(t,e){fetch("/pos/api/quotations/"+t+"/print",{method:"POST",headers:{Authorization:"Bearer "+e,"Content-Type":"application/json"},body:JSON.stringify({printer_type:"browser"})}).then((function(t){return t.json()})).then((function(o){var n="Cotización #"+o.id+"";n+="",n+="

COTIZACIÓN

",n+='

COT-'+o.id+"

",n+="

Fecha: "+(o.created_at||"").substring(0,10)+"

",o.customer_name&&(n+="

Cliente: "+o.customer_name+"

"),o.wa_phone&&(n+="

WhatsApp: "+o.wa_phone+"

"),n+="
",(o.items||[]).forEach((function(t){n+="",t.part_number&&(n+='")})),n+="
"+t.quantity+"x "+t.name+'$'+t.subtotal.toFixed(2)+"
#'+t.part_number+"

",n+='

Subtotal: $'+o.subtotal.toFixed(2)+"

",n+='

IVA: $'+o.tax_total.toFixed(2)+"

",n+='

TOTAL: $'+o.total.toFixed(2)+"

",n+='

Esta cotización no es comprobante fiscal
Precios sujetos a disponibilidad

',n+="";var a=window.open("","_blank","width=400,height=600");a.document.write(n),a.document.close(),setTimeout((function(){a.print()}),500),r(t,e)})).catch((function(t){console.error("[auto-print] Browser print failed:",t)}))}function r(t,e){fetch("/pos/api/quotations/"+t+"/mark-printed",{method:"POST",headers:{Authorization:"Bearer "+e}}).catch((function(){}))}(window.startAutoPrint=function(){if(!o){0;var t=localStorage.getItem("pos_token");t&&(o=setInterval((function(){fetch("/pos/api/quotations/print-queue",{headers:{Authorization:"Bearer "+t}}).then((function(t){return t.json()})).then((function(e){e.data&&e.data.length&&e.data.forEach((function(e){console.log("[auto-print] Cotización #"+e.id+" confirmada por WhatsApp — imprimiendo..."),showToast("🖨️ Imprimiendo cotización #"+e.id+" (WhatsApp)","ok"),function(t,e){"undefined"!=typeof NexusPrinter&&NexusPrinter.isConnected&&NexusPrinter.isConnected()?fetch("/pos/api/quotations/"+t+"/print",{method:"POST",headers:{Authorization:"Bearer "+e,"Content-Type":"application/json"},body:JSON.stringify({printer_type:"escpos_raw",width:80})}).then((function(t){return t.arrayBuffer()})).then((function(o){NexusPrinter.sendRaw(new Uint8Array(o)),r(t,e)})).catch((function(o){console.error("[auto-print] Thermal print failed:",o),n(t,e)})):n(t,e)}(e.id,t)}))})).catch((function(){}))}),15e3),console.log("[auto-print] Enabled — polling every 15s"))}},window.stopAutoPrint=function(){o&&(clearInterval(o),o=null)},-1!==window.location.pathname.indexOf("/pos/sale")||-1!==window.location.pathname.indexOf("/pos/quotation")||-1!==window.location.pathname.indexOf("/pos/dashboard"))&&(localStorage.getItem("pos_token")&&setTimeout((function(){startAutoPrint()}),3e3));if(!document.getElementById("pos-utils-styles")){var a=document.createElement("style");a.id="pos-utils-styles",a.textContent="@keyframes slideInRight{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}.filter-panel select:focus{outline:none;border-color:var(--color-primary,#F5A623);box-shadow:0 0 0 2px var(--glow-color-soft,rgba(245,166,35,0.15));}",document.head.appendChild(a)}}(); \ No newline at end of file diff --git a/pos/static/js/pos.min.js b/pos/static/js/pos.min.js new file mode 100644 index 0000000..9fa3744 --- /dev/null +++ b/pos/static/js/pos.min.js @@ -0,0 +1 @@ +const POS=(()=>{let e=localStorage.getItem("pos_token")||"",t=[],n=-1,o=null,a=null,i="efectivo",r=!1,c=100,s=null,l=null,d=null,m=null;const u=localStorage.getItem("pos_currency")||"MXN",p={MXN:"$",USD:"US$"},y="USD"===u?"en-US":"es-MX",v=e=>(p[u]||"$")+parseFloat(e||0).toLocaleString(y,{minimumFractionDigits:2,maximumFractionDigits:2});async function g(t,n={}){n.headers={"Content-Type":"application/json",Authorization:"Bearer "+e};const o=await fetch(t,n),a=await o.json();if(!o.ok)throw new Error(a.error||`HTTP ${o.status}`);return a}function f(e){const t=document.getElementById("toastContainer");if(!t)return;const n=document.createElement("div");n.className="toast",n.textContent=e,t.appendChild(n),setTimeout((()=>n.remove()),2100)}function h(e){const n=t.find((t=>t.inventory_id===e.inventory_id));if(n)return n.quantity+=e.quantity||1,void E();t.push({inventory_id:e.inventory_id||e.id,part_number:e.part_number||"",name:e.name||"",quantity:e.quantity||1,unit_price:parseFloat(e.unit_price||e.price_1||0),unit_cost:parseFloat(e.cost||0),discount_pct:parseFloat(e.discount_pct||0),tax_rate:parseFloat(e.tax_rate||.16),stock:e.stock||0}),E(),f(`${e.name||"Articulo"} agregado`)}function _(e){t.splice(e,1),n>=t.length&&(n=t.length-1),E()}function E(){const e=document.getElementById("cartBody"),o=document.getElementById("cartTable"),a=document.getElementById("cartEmpty");if(0===t.length)return o.style.display="none",a.style.display="flex",void b();o.style.display="",a.style.display="none";let i="";t.forEach(((e,t)=>{const o=e.unit_price*e.quantity,a=o-o*e.discount_pct/100,c=r?`${v(e.unit_cost)}`:"";let s="";if(r){const t=e.unit_price*(1-e.discount_pct/100),n=t>0?((t-e.unit_cost)/t*100).toFixed(1):"0.0",o=parseFloat(n);s=`15?"var(--color-warning)":"var(--color-error)"};">${n}%`}i+=`\n ${t+1}\n \n
${e.name}
\n
${e.part_number} | Stock: ${e.stock}
\n \n \n ${v(e.unit_price)}\n %\n ${v(a)}\n ${c}\n ${s}\n \n `})),e.innerHTML=i,b(),function(){const e=document.getElementById("avgMargin");if(!e||!r)return;let n=0,o=0;if(t.forEach((e=>{if(e.unit_cost>0){const t=e.unit_price*(1-e.discount_pct/100);n+=t*e.quantity,o+=e.unit_cost*e.quantity}})),n>0){const t=(n-o)/n*100;e.textContent=t.toFixed(1)+"%",e.style.color=t>30?"var(--color-success)":t>15?"var(--color-warning)":"var(--color-error)"}else e.textContent="--"}()}function I(e,n){let o=Math.max(0,Math.min(100,parseFloat(n)||0));o>c&&(alert(`Descuento maximo permitido: ${c}%`),o=c),t[e].discount_pct=o,E()}function b(){let e=0,n=0,o=0;t.forEach((t=>{const a=t.unit_price*t.quantity,i=a*t.discount_pct/100,r=a-i,c=r*t.tax_rate;e+=r,n+=i,o+=c}));const a=e+o;document.getElementById("dispSubtotal").textContent=v(e),document.getElementById("dispTax").textContent=v(o),document.getElementById("dispTotal").textContent=v(a),n>0?(document.getElementById("discountRow").style.display="",document.getElementById("dispDiscount").textContent="-"+v(n)):document.getElementById("discountRow").style.display="none"}function x(){let e=0,n=0;return t.forEach((t=>{const o=t.unit_price*t.quantity,a=o-o*t.discount_pct/100,i=a*t.tax_rate;e+=a,n+=i})),Math.round(100*(e+n))/100}async function B(e){if(!e||e.length<2)$();else try{const t=await g(`/pos/api/inventory/items?q=${encodeURIComponent(e)}&per_page=20`),n=document.getElementById("searchResults"),a=document.getElementById("totalsPanel");if(0===t.data.length)n.innerHTML='
Sin resultados
';else{let e="";t.data.forEach((t=>{let n=t.price_1;if(o){const e=o.price_tier||1;n=3===e?t.price_3:2===e?t.price_2:t.price_1}e+=`
\n
\n
${t.name}
\n
${t.part_number} | ${t.brand||""}
\n
Stock: ${t.stock}
\n
\n
${v(n)}
\n
`})),n.innerHTML=e}n.style.display="",a.style.display="none"}catch(e){console.error("Search error:",e)}}function $(){const e=document.getElementById("searchResults"),t=document.getElementById("totalsPanel");e&&(e.style.display="none"),t&&(t.style.display="")}async function C(e){o=e,document.getElementById("customerAutocomplete").style.display="none",document.getElementById("customerSearchWrap").querySelector("input").style.display="none";if(document.getElementById("customerName").textContent=e.name,document.getElementById("customerTier").textContent={1:"P1 Mostrador",2:"P2 Taller",3:"P3 Mayoreo"}[e.price_tier]||"P1",document.getElementById("customerCredit").textContent=`Credito: ${v(e.credit_balance)} / ${v(e.credit_limit)}`,document.getElementById("customerSelected").style.display="",e.vehicle_info&&e.vehicle_info.length>0){const t=e.vehicle_info[0];document.getElementById("vehicleInfo").textContent=`${t.make||""} ${t.model||""} ${t.year||""} ${t.plates?"("+t.plates+")":""}`,document.getElementById("vehicleBanner").classList.add("visible")}try{const t=await g(`/pos/api/customers/${e.id}`);if(t.recent_purchases&&t.recent_purchases.length>0){const e=t.recent_purchases[0],n=Math.floor((Date.now()-new Date(e.created_at).getTime())/864e5),o=0===n?"hoy":1===n?"hace 1 dia":`hace ${n} dias`;document.getElementById("lastPurchaseInfo").textContent=`${v(e.total)} ${o}`,document.getElementById("vehicleBanner").classList.add("visible")}}catch(e){console.warn("Could not fetch customer detail:",e)}const t=document.getElementById("cfdiHint");t&&e.rfc&&(t.textContent=`RFC: ${e.rfc}`,document.getElementById("cfdiCheck").checked=!!e.rfc),E()}function w(){o=null,document.getElementById("customerSelected").style.display="none";const e=document.getElementById("customerSearch");e&&(e.style.display="",e.value=""),document.getElementById("vehicleBanner").classList.remove("visible");const t=document.getElementById("cfdiHint");t&&(t.textContent=""),E()}function k(){document.getElementById("newCustomerModal").classList.remove("open")}function S(){if(0===t.length)return void f("Carrito vacio");if(!a)return void alert("No hay caja abierta. Abra una caja primero.");i="efectivo";const e=x();document.getElementById("modalTotal").textContent=v(e),document.getElementById("modalItemCount").textContent=`${t.reduce(((e,t)=>e+t.quantity),0)} productos`,document.getElementById("modalCustomerName").textContent=o?o.name:"Publico General",document.getElementById("cashReceived").value="",document.getElementById("changeDisplay").textContent="$0.00",document.getElementById("changeDisplay").className="cambio-amount positive";const n=document.getElementById("paymentRef");n&&(n.value="");const r=document.getElementById("quickAmounts");if(r){const t=[Math.ceil(e),100*Math.ceil(e/100),500*Math.ceil(e/500),1e3*Math.ceil(e/1e3)],n=[...new Set(t)].slice(0,4);r.innerHTML=n.map((e=>``)).join("")}const c=document.getElementById("refAmount");c&&(c.value=v(e)),document.querySelectorAll(".pago-tab").forEach((e=>e.classList.remove("active"))),document.querySelector('.pago-tab[data-method="efectivo"]').classList.add("active"),document.getElementById("cashPayment").classList.add("active"),document.getElementById("cashPayment").style.display="",document.getElementById("refPayment").classList.remove("active"),document.getElementById("refPayment").style.display="none",document.getElementById("mixedPayment").classList.remove("active"),document.getElementById("mixedPayment").style.display="none";const s=document.getElementById("btnConfirmPayment");s.disabled=!1,s.textContent=`Confirmar Pago — ${v(e)}`,document.getElementById("paymentModal").classList.add("open"),setTimeout((()=>document.getElementById("cashReceived").focus()),100)}function P(){document.getElementById("paymentModal").classList.remove("open")}async function T(){const e=x();let r=0,c=[],d="";if("efectivo"===i){if(r=parseFloat(document.getElementById("cashReceived").value)||0,r{const t=e.querySelector("select").value,n=parseFloat(e.querySelector(".mixed-amount").value)||0,o=e.querySelectorAll("input")[1]?.value||"";n>0&&(c.push({method:t,amount:n,reference:o}),r+=n)})),r({inventory_id:e.inventory_id,quantity:e.quantity,unit_price:e.unit_price,discount_pct:e.discount_pct,tax_rate:e.tax_rate}))),customer_id:o?o.id:null,payment_method:i,sale_type:"cash",register_id:a?a.id:null,amount_paid:r,payment_details:c,reference:d,generate_cfdi:document.getElementById("cfdiCheck").checked},u=document.getElementById("btnConfirmPayment");u.disabled=!0,u.textContent="Procesando...";try{const e=await g("/pos/api/sales",{method:"POST",body:JSON.stringify(m)});s=e.id,l=e,P(),L(e),t=[],n=-1,w(),E(),f(`Venta #${e.id} completada`)}catch(e){alert("Error al procesar venta: "+e.message)}finally{u.disabled=!1,u.textContent="Confirmar Pago"}}async function M(){if(0===t.length)return void f("Carrito vacio");const e={items:t.map((e=>({inventory_id:e.inventory_id,quantity:e.quantity,unit_price:e.unit_price,discount_pct:e.discount_pct,tax_rate:e.tax_rate}))),customer_id:o?o.id:null};try{const t=await g("/pos/api/quotations",{method:"POST",body:JSON.stringify(e)});f(`Cotizacion #${t.id} guardada. Total: ${v(t.total)}`)}catch(e){alert("Error: "+e.message)}}function L(e){const t=new Date(e.created_at).toLocaleString("es-MX",{year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"}),n=o?o.name:"Publico General",r=o&&o.rfc?o.rfc:"";let c="";(e.items||[]).forEach((e=>{const t=e.unit_price*e.quantity*(1-(e.discount_pct||0)/100);c+=`\n
\n ${e.quantity}\n ${e.name||""}\n ${v(e.unit_price)}\n ${v(e.subtotal||t)}\n
`}));const s=`\n
NEXUS AUTOPARTS
\n
Tu conexion con las refacciones
\n
\n Sucursal: ${a&&a.branch_name||""}
\n RFC: NAU210315XX1\n
\n
\n
\n VENTA: V-${e.id}\n ${t}\n
\n
\n Cliente: ${n}\n ${r?`RFC: ${r}`:""}\n
\n
\n
\n Cant\n Descripcion\n P. Unit\n Importe\n
\n
\n ${c}\n
\n
\n
\n Subtotal:${v(e.subtotal)}\n
\n ${e.discount_total>0?`
Descuento:-${v(e.discount_total)}
`:""}\n
\n IVA 16%:${v(e.tax_total)}\n
\n
\n TOTAL:${v(e.total)}\n
\n
\n
\n
\n
\n Forma de pago:${e.payment_method||i}\n
\n ${"efectivo"===e.payment_method?`\n
\n Recibido:${v(e.amount_paid)}\n
\n
\n Cambio:${v(e.change_given||0)}\n
`:""}\n
\n
\n \n `,l=document.getElementById("ticketContent");l&&(l.innerHTML=s);const d=document.getElementById("ticketPreviewContent");d&&(d.innerHTML=s),document.getElementById("ticketModal").classList.add("open")}function q(){document.getElementById("ticketModal").classList.remove("open")}async function N(){if(s)try{L(await g(`/pos/api/sales/${s}`))}catch(e){alert("Error: "+e.message)}else f("No hay venta reciente")}function D(){f("Comando enviado al cajon de efectivo.")}return async function(){try{const t=JSON.parse(atob(e.split(".")[1]));if(document.getElementById("employeeName").textContent=t.name||"Empleado",document.getElementById("branchName").textContent=t.branch_name||"",r=(t.permissions||[]).includes("pos.view_cost"),c=t.max_discount_pct||100,r){document.getElementById("thCost").style.display="",document.getElementById("thMargin").style.display="";const e=document.getElementById("costToggle");e&&(e.style.display="")}const n=document.querySelector(".status-bar__user-avatar");if(n&&t.name){const e=t.name.split(" ");n.textContent=e.map((e=>e[0])).join("").substring(0,2).toUpperCase()}}catch(e){console.warn("Could not parse token:",e)}const o=localStorage.getItem("pos_cart");if(o)try{const e=JSON.parse(o);for(const t of e)h(t);localStorage.removeItem("pos_cart")}catch(e){console.warn("Could not load catalog cart:",e)}await async function(){try{const e=await g("/pos/api/register/current");e.register?(a=e.register,document.getElementById("registerInfo").innerHTML=`Caja #${e.register.register_number}`):document.getElementById("registerInfo").innerHTML="Sin caja abierta"}catch(e){console.warn("Register check failed:",e)}}(),document.addEventListener("keydown",(e=>{const o=e.target.tagName,a="INPUT"===o||"TEXTAREA"===o||"SELECT"===o;switch(e.key){case"F1":e.preventDefault(),document.getElementById("itemSearch").focus();break;case"F2":e.preventDefault(),document.getElementById("customerSearch").focus(),document.getElementById("customerSearch").style.display="",document.getElementById("customerSelected").style.display="none";break;case"F3":e.preventDefault(),S();break;case"F4":e.preventDefault(),M();break;case"F5":e.preventDefault(),N();break;case"F6":e.preventDefault(),D();break;case"Escape":if(e.preventDefault(),document.getElementById("paymentModal").classList.contains("open"))P();else if(document.getElementById("newCustomerModal").classList.contains("open"))k();else if(document.getElementById("ticketModal").classList.contains("open"))q();else if(document.querySelector(".confirm-overlay.active")){const e=document.getElementById("overlay-cancelar-venta"),t=document.getElementById("modal-cancelar-venta");e&&e.classList.contains("active")&&(e.classList.remove("active"),t.classList.remove("active"))}else $();break;case"Delete":!a&&n>=0&&n=0&&n=0&&n1&&(t[n].quantity--,E()));break;case"*":if(!a&&n>=0&&n0&&(e.preventDefault(),n=Math.max(0,n-1),E());break;case"ArrowDown":!a&&t.length>0&&(e.preventDefault(),n=Math.min(t.length-1,n+1),E());break;case"Enter":"cashReceived"===e.target.id&&(e.preventDefault(),T())}})),function(){const e=document.getElementById("itemSearch");if(!e)return;e.addEventListener("input",(()=>{clearTimeout(d),d=setTimeout((()=>B(e.value.trim())),300)})),e.addEventListener("keydown",(t=>{"Enter"===t.key&&(t.preventDefault(),B(e.value.trim())),"Escape"===t.key&&(e.value="",$())}))}(),function(){const e=document.getElementById("customerSearch");if(!e)return;e.addEventListener("input",(()=>{clearTimeout(m),m=setTimeout((()=>async function(e){if(!e||e.length<2)return void(document.getElementById("customerAutocomplete").style.display="none");try{const t=await g(`/pos/api/customers?q=${encodeURIComponent(e)}&per_page=10`),n=document.getElementById("customerAutocomplete");if(0===t.data.length)n.innerHTML='
Sin resultados
';else{let e="";t.data.forEach((t=>{const n={1:"Mostrador",2:"Taller",3:"Mayoreo"};e+=`
\n
${t.name}
\n
${t.rfc||""} | ${t.phone||""} | ${n[t.price_tier]||"P1"} | Credito: ${v(t.credit_balance)}/${v(t.credit_limit)}
\n
`})),n.innerHTML=e}n.style.display="block"}catch(e){console.error("Customer search error:",e)}}(e.value.trim())),300)})),e.addEventListener("keydown",(t=>{"Escape"===t.key&&(e.value="",document.getElementById("customerAutocomplete").style.display="none")}))}()}(),{addToCart:h,removeFromCart:_,selectRow:function(e){n=e,E()},updateQty:function(e,n){const o=Math.max(1,parseInt(n)||1);t[e].quantity=o,E()},updateDiscount:I,addFromSearch:function(e,t){h({inventory_id:e.id,part_number:e.part_number,name:e.name,unit_price:t,cost:e.cost,tax_rate:e.tax_rate,stock:e.stock}),$(),document.getElementById("itemSearch").value="",document.getElementById("itemSearch").focus()},hideSearchResults:$,selectCustomer:C,clearCustomer:w,showNewCustomerModal:function(){document.getElementById("newCustomerModal").classList.add("open"),setTimeout((()=>document.getElementById("ncName").focus()),100)},closeNewCustomerModal:k,saveNewCustomer:async function(){const e=document.getElementById("ncName").value.trim();if(!e)return void alert("Nombre es requerido");const t=[],n=document.getElementById("ncVehMake").value.trim();n&&t.push({make:n,model:document.getElementById("ncVehModel").value.trim(),year:document.getElementById("ncVehYear").value.trim(),plates:document.getElementById("ncVehPlates").value.trim()});const o={name:e,rfc:document.getElementById("ncRfc").value.trim()||null,razon_social:document.getElementById("ncRazonSocial").value.trim()||null,regimen_fiscal:document.getElementById("ncRegimenFiscal").value||null,uso_cfdi:document.getElementById("ncUsoCfdi").value||"G03",phone:document.getElementById("ncPhone").value.trim()||null,email:document.getElementById("ncEmail").value.trim()||null,price_tier:parseInt(document.getElementById("ncPriceTier").value)||1,credit_limit:parseFloat(document.getElementById("ncCreditLimit").value)||0,vehicle_info:t.length>0?t:null};try{C({id:(await g("/pos/api/customers",{method:"POST",body:JSON.stringify(o)})).id,name:o.name,rfc:o.rfc,phone:o.phone,price_tier:o.price_tier,credit_limit:o.credit_limit,credit_balance:0,vehicle_info:o.vehicle_info}),k(),f("Cliente creado")}catch(e){alert("Error al crear cliente: "+e.message)}},checkout:S,confirmPayment:T,closePaymentModal:P,selectPaymentMethod:function(e,t){i=e,document.querySelectorAll(".pago-tab").forEach((e=>e.classList.remove("active"))),t&&t.classList.add("active");const n={efectivo:"cashPayment",transferencia:"refPayment",tarjeta:"refPayment",mixto:"mixedPayment"};if(["cashPayment","refPayment","mixedPayment"].forEach((t=>{const o=document.getElementById(t);if(o){const t=o.id===n[e];o.classList.toggle("active",t),o.style.display=t?"":"none"}})),"efectivo"===e&&document.getElementById("cashReceived").focus(),"transferencia"===e||"tarjeta"===e){const e=document.getElementById("paymentRef");e&&e.focus()}},updateChange:function(){const e=x(),t=(parseFloat(document.getElementById("cashReceived").value)||0)-e,n=document.getElementById("changeDisplay");t>=0?(n.textContent=v(t),n.className="cambio-amount positive"):(n.textContent="-"+v(Math.abs(t)),n.className="cambio-amount negative")},updateMixedTotal:function(){const e=x();let t=0;document.querySelectorAll(".mixed-amount").forEach((e=>{t+=parseFloat(e.value)||0}));const n=e-t,o=document.getElementById("mixedRemaining");o.textContent=n>0?`Faltante: ${v(n)}`:`Cubierto (${v(t)})`,o.style.color=n>0?"var(--color-error)":"var(--color-success)"},creditSale:async function(){if(0===t.length)return void alert("Carrito vacio");if(!o)return void alert("Seleccione un cliente para venta a credito");if(!a)return void alert("No hay caja abierta.");const e=x(),i=(o.credit_limit||0)-(o.credit_balance||0);if(o.credit_limit>0&&e>i&&!confirm(`Credito insuficiente. Disponible: ${v(i)}, Total: ${v(e)}. Continuar?`))return;const r={items:t.map((e=>({inventory_id:e.inventory_id,quantity:e.quantity,unit_price:e.unit_price,discount_pct:e.discount_pct,tax_rate:e.tax_rate}))),customer_id:o.id,payment_method:"credito",sale_type:"credit",register_id:a?a.id:null,amount_paid:0};try{const e=await g("/pos/api/sales",{method:"POST",body:JSON.stringify(r)});s=e.id,l=e,L(e),t=[],n=-1,w(),E()}catch(e){alert("Error: "+e.message)}},saveQuotation:M,createLayaway:async function(){if(0===t.length)return void alert("Carrito vacio");if(!o)return void alert("Seleccione un cliente para apartado");const e=x(),i=prompt(`Total: ${v(e)}\nIngrese monto del anticipo:`);if(!i)return;const r=parseFloat(i);if(isNaN(r)||r<=0)return void alert("Monto invalido");if(r>e)return void alert("El anticipo no puede exceder el total");const c={items:t.map((e=>({inventory_id:e.inventory_id,quantity:e.quantity,unit_price:e.unit_price,discount_pct:e.discount_pct,tax_rate:e.tax_rate}))),customer_id:o.id,initial_payment:r,payment_method:"efectivo",register_id:a?a.id:null};try{const e=await g("/pos/api/layaways",{method:"POST",body:JSON.stringify(c)});f(`Apartado #${e.id} creado. Restante: ${v(e.remaining)}`),t=[],n=-1,w(),E()}catch(e){alert("Error: "+e.message)}},showLastSale:N,openDrawer:D,showTicket:L,closeTicketModal:q,printTicket:function(){const e=document.getElementById("ticketPrintArea");e&&(e.style.display="block"),window.print(),setTimeout((()=>{e&&(e.style.display="none")}),500)},connectThermal:async function(){if(!window.NexusPrinter)return void f("Printer module not loaded");const e=await NexusPrinter.connect();e.ok?(f("Impresora conectada: "+(e.name||e.type)),function(){const e=document.getElementById("btnConnectPrinter"),t=document.getElementById("btnThermalPrint");window.NexusPrinter&&NexusPrinter.isConnected()?(e&&(e.style.display="none"),t&&(t.style.display="")):(e&&(e.style.display=""),t&&(t.style.display="none"))}()):f(e.error||"No se pudo conectar la impresora")},thermalPrint:async function(){if(!window.NexusPrinter||!NexusPrinter.isConnected())return void f("Conecte una impresora termica primero");if(!s)return void f("No hay venta para imprimir");f(await NexusPrinter.printSale(s)?"Ticket enviado a impresora termica":"Error al imprimir. Reconecte la impresora.")}}})(); \ No newline at end of file diff --git a/pos/static/js/printer.min.js b/pos/static/js/printer.min.js new file mode 100644 index 0000000..91a1498 --- /dev/null +++ b/pos/static/js/printer.min.js @@ -0,0 +1 @@ +window.NexusPrinter=function(){"use strict";let e=null,t=null,r=null,n=null,a=null;async function i(){try{"usb"===a&&e&&await e.close(),"serial"===a&&(n&&(n.releaseLock(),n=null),r&&(await r.close(),r=null))}catch(e){}e=null,t=null,a=null,localStorage.removeItem("nexus_printer")}async function o(r){if(!a)return!1;try{if("usb"===a&&e&&null!==t){const n=512;for(let a=0;a"out"===e.direction&&"bulk"===e.type));if(n)return t=n.endpointNumber,a="usb",l(),{ok:!0,type:"usb",name:e.productName||"USB Printer"}}catch(t){e=null}if("serial"in navigator)try{return r=await navigator.serial.requestPort(),await r.open({baudRate:9600}),n=r.writable.getWriter(),a="serial",l(),{ok:!0,type:"serial",name:"Serial Printer"}}catch(e){r=null,n=null}return{ok:!1,error:"No printer connected. Use a browser that supports WebUSB or Web Serial (Chrome/Edge)."}},disconnect:i,isConnected:function(){return null!==a},sendRaw:o,printSale:async function(e,t){t=t||80;const r=localStorage.getItem("pos_token"),n=await fetch("/pos/api/sales/"+e+"/print",{method:"POST",headers:{Authorization:"Bearer "+r,"Content-Type":"application/json"},body:JSON.stringify({printer_type:"escpos_raw",width:t})});if(!n.ok)return console.error("[NexusPrinter] backend error",n.status),!1;const a=await n.arrayBuffer();return o(new Uint8Array(a))}}}(); \ No newline at end of file diff --git a/pos/static/js/push.min.js b/pos/static/js/push.min.js new file mode 100644 index 0000000..c86c566 --- /dev/null +++ b/pos/static/js/push.min.js @@ -0,0 +1 @@ +!function(){"use strict";var e={};try{e=JSON.parse(localStorage.getItem("pos_employee")||"{}")}catch(e){}var o=e.role||"";if("owner"===o||"admin"===o)if("serviceWorker"in navigator&&"PushManager"in window){var r=localStorage.getItem("pos_token");r&&("complete"===document.readyState?setTimeout(i,2e3):window.addEventListener("load",(function(){setTimeout(i,2e3)})))}else console.log("[Push] Browser does not support push notifications");function t(e){for(var o=(e+"=".repeat((4-e.length%4)%4)).replace(/-/g,"+").replace(/_/g,"/"),r=window.atob(o),t=new Uint8Array(r.length),i=0;i{function t(){return localStorage.getItem("pos_token")||""}function a(t){return parseFloat(t||0).toLocaleString("es-MX",{minimumFractionDigits:2,maximumFractionDigits:2})}function e(t){return parseInt(t||0).toLocaleString("es-MX")}function n(t){if(!t)return"--";var a=new Date(t);return isNaN(a)?t:a.toLocaleDateString("es-MX",{day:"2-digit",month:"short",year:"numeric"})+" "+a.toLocaleTimeString("es-MX",{hour:"2-digit",minute:"2-digit"})}function r(t){return'
'+t+"
"}function o(t){return'
'+t+"
"}var d={ventas:!1,inventario:!1,clientes:!1,financieros:!1};function l(t){document.documentElement.setAttribute("data-theme",t);try{localStorage.setItem("nexus-theme",t)}catch(t){}var a=document.getElementById("btn-industrial"),e=document.getElementById("btn-modern");a&&a.classList.toggle("is-active","industrial"===t),e&&e.classList.toggle("is-active","modern"===t)}function s(t,a){document.querySelectorAll(".tab-panel").forEach((function(t){t.classList.remove("is-active")})),document.querySelectorAll(".tab-btn").forEach((function(t){t.classList.remove("is-active")}));var e=document.getElementById("panel-"+t);e&&e.classList.add("is-active"),a&&a.classList.add("is-active"),d[t]||("ventas"===t?h():"inventario"===t?v():"clientes"===t?m():"financieros"===t&&p())}function i(){var t=document.getElementById("live-clock");if(t){var a=new Date,e=function(t){return String(t).padStart(2,"0")};t.textContent=e(a.getHours())+":"+e(a.getMinutes())+":"+e(a.getSeconds())}}async function c(a){var e=await fetch(a,{headers:{Authorization:`Bearer ${t()}`,"Content-Type":"application/json"}});if(!e.ok)throw new Error("HTTP "+e.status);return e.json()}function g(t,a,e){return'
'+t+'
'+a+"
"+(e?'
'+e+"
":"")+"
"}async function h(){d.ventas=!0;var t=document.getElementById("ventas-date-from").value,l=document.getElementById("ventas-date-to").value,s=new URLSearchParams;t&&s.set("date_from",t),l&&s.set("date_to",l),s.set("per_page","200");var i=document.getElementById("ventas-kpis"),h=document.getElementById("ventas-bar-chart"),v=document.getElementById("ventas-por-vendedor"),m=document.getElementById("ventas-por-metodo"),p=document.getElementById("ventas-detalle");i.innerHTML='
Cargando...
',h.innerHTML="",v.innerHTML='
Cargando...
',m.innerHTML='
Cargando...
',p.innerHTML='
Cargando...
';try{for(var u=[],y=1,b=1;y<=b;){s.set("page",y);var f=await c("/pos/api/sales?"+s.toString());if(u=u.concat(f.data||[]),b=f.pagination?f.pagination.total_pages:1,++y>50)break}var _=u.filter((function(t){return"completed"===t.status})),T=_.reduce((function(t,a){return t+a.total}),0),M=_.length,x=M>0?T/M:0;i.innerHTML=g("Total Ventas","$"+a(T),M+" transacciones")+g("Ticket Promedio","$"+a(x),"")+g("Transacciones",e(M),"")+g("Descuentos","$"+a(_.reduce((function(t,a){return t+a.discount_total}),0)),"");var L={},E=["Dom","Lun","Mar","Mie","Jue","Vie","Sab"];_.forEach((function(t){var a=t.created_at.substring(0,10);L[a]=(L[a]||0)+t.total}));var C=Object.keys(L).sort().slice(-7),w=Math.max.apply(null,C.map((function(t){return L[t]})))||1;if(C.length>0){var $='
Ventas por Dia
';C.forEach((function(t){var e=L[t],n=Math.round(e/w*100),r=E[new Date(t+"T12:00:00").getDay()],o=e>=1e3?"$"+(e/1e3).toFixed(1)+"k":"$"+a(e);$+='
'+o+'
'+r+"
"})),$+="
",h.innerHTML=$}var S={};_.forEach((function(t){var a=t.employee_id||0;S[a]||(S[a]={name:t.employee_name||"Sin asignar",count:0,total:0}),S[a].count++,S[a].total+=t.total}));var I=Object.values(S).sort((function(t,a){return a.total-t.total})),H='
Ventas por Vendedor
';H+='
',I.forEach((function(t){var e=t.name.split(" ").map((function(t){return t[0]})).join("").substring(0,2).toUpperCase();H+='"})),H+="
Vendedor# VentasTotalTicket Prom.
'+e+'
'+t.name+'
'+t.count+'$'+a(t.total)+'$'+a(t.count>0?t.total/t.count:0)+"
",v.innerHTML=I.length?H:r("Sin datos de vendedores");var k={};_.forEach((function(t){var a=t.payment_method||"Otro";k[a]=(k[a]||0)+t.total}));var B=Object.entries(k).sort((function(t,a){return a[1]-t[1]})),A=(B.length>0&&B[0][1],{cash:"Efectivo",card:"Tarjeta",transfer:"Transferencia",credit:"Credito",mixed:"Mixto"}),D=["","pay-method__bar--b","pay-method__bar--c","pay-method__bar--d"],P='
Ventas por Metodo de Pago
';P+='
',B.forEach((function(t,e){var n=T>0?Math.round(t[1]/T*100):0,r=A[t[0]]||t[0];P+='
'+r+'
$'+a(t[1])+' '+n+"%
"})),P+="
",m.innerHTML=B.length?P:r("Sin datos de metodos");var V='
Detalle de Ventas'+u.length+" registros
";V+='
',u.slice(0,100).forEach((function(t){var e="completed"===t.status?"pill--success":"cancelled"===t.status?"pill--error":"pill--warning",r="completed"===t.status?"Completada":"cancelled"===t.status?"Cancelada":t.status;V+='"})),V+="
#FechaVendedorClientePagoSubtotalDesc.IVATotalEstado
'+t.id+""+n(t.created_at)+""+(t.employee_name||"--")+""+(t.customer_name||"Mostrador")+""+(A[t.payment_method]||t.payment_method||"--")+'$'+a(t.subtotal)+'$'+a(t.discount_total)+'$'+a(t.tax_total)+'$'+a(t.total)+''+r+"
",p.innerHTML=V}catch(t){i.innerHTML=o("Error cargando ventas: "+t.message),v.innerHTML="",m.innerHTML="",p.innerHTML=""}}async function v(){d.inventario=!0;var t=document.getElementById("inventario-kpis"),n=document.getElementById("inventario-valorizacion"),l=document.getElementById("inventario-abc"),s=document.getElementById("inventario-low-stock"),i=document.getElementById("inventario-no-movement");t.innerHTML='
Cargando...
',n.innerHTML='
Cargando...
',l.innerHTML='
Cargando...
',s.innerHTML='
Cargando...
',i.innerHTML='
Cargando...
';try{var[h,v,m,p]=await Promise.all([c("/pos/api/inventory/reports/valuation"),c("/pos/api/inventory/reports/abc"),c("/pos/api/inventory/reports/low-stock"),c("/pos/api/inventory/reports/no-movement")]);t.innerHTML=g("Valor Total Inventario","$"+a(h.grand_total),e(h.item_count)+" SKUs activos")+g("Clasificacion A",e(v.summary.A)+" SKUs","80% del volumen de ventas")+g("Stock Bajo",e(m.count)+" productos","debajo del minimo")+g("Sin Movimiento",e(p.count)+" productos",">"+p.days_threshold+" dias");var u=(h.data||[]).slice(0,20),y='
Inventario ValorizadoTop 20 de '+e(h.item_count)+"
";y+='
',u.forEach((function(t){y+='"})),y+="
ProductoNo. ParteMarcaStockCosto Unit.Valor
'+t.name+''+(t.part_number||"--")+""+(t.brand||"--")+''+e(t.stock)+'$'+a(t.cost)+'$'+a(t.value)+"
",n.innerHTML=y;var b=(v.data||[]).slice(0,30),f='
Clasificacion ABC de InventarioA: '+v.summary.A+' B: '+v.summary.B+' C: '+v.summary.C+"
";f+='
',b.forEach((function(t){var a="A"===t.classification?"pill--success":"B"===t.classification?"pill--warning":"pill--muted";f+='"})),f+="
ProductoNo. ParteMarcaVol. Ventas% Acum.Clase
'+t.name+''+(t.part_number||"--")+""+(t.brand||"--")+''+e(t.sales_volume)+''+t.cumulative_pct+'%'+t.classification+"
",l.innerHTML=f;var _=m.data||[],T='
Productos con Stock Bajo'+m.count+" productos
";T+='
',_.slice(0,30).forEach((function(t){var a=t.stock<=0||t.stock"})),T+="
ProductoNo. ParteMarcaStockMinimoDeficit
'+(t.part_number||"--")+""+(t.brand||"--")+''+e(t.stock)+''+e(t.min_stock)+''+e(t.deficit)+"
",s.innerHTML=_.length?T:r("No hay productos con stock bajo");var M=p.data||[],x='
Productos Sin Movimiento (>'+p.days_threshold+' dias)'+p.count+" SKUs
";x+='
',M.slice(0,30).forEach((function(t){x+='"})),x+="
ProductoNo. ParteMarcaStockCosto Unit.Ultimo Movimiento
'+t.name+''+(t.part_number||"--")+""+(t.brand||"--")+''+e(t.stock)+'$'+a(t.cost)+''+function(t){if(!t)return"--";var a=new Date(t);return isNaN(a)?t:a.toLocaleDateString("es-MX",{day:"2-digit",month:"short",year:"numeric"})}(t.last_movement)+"
",i.innerHTML=M.length?x:r("No hay productos sin movimiento")}catch(a){t.innerHTML=o("Error cargando inventario: "+a.message),n.innerHTML="",l.innerHTML="",s.innerHTML="",i.innerHTML=""}}async function m(){d.clientes=!0;var t=document.getElementById("clientes-kpis"),n=document.getElementById("clientes-aging");t.innerHTML='
Cargando...
',n.innerHTML='
Cargando...
';try{var l=await c("/pos/api/accounting/aging"),s=l.data||[],i=l.totals||{};t.innerHTML=g("Clientes con Credito",e(s.length),"con saldo pendiente")+g("Saldo Total","$"+a(i.total),"")+g("Corriente","$"+a(i.corriente),"no vencido")+g("Vencido >90 dias","$"+a(i.d90_plus),i.d90_plus>0?'requiere atencion':"");var h='
Antiguedad de Saldos'+s.length+" clientes
";h+='
',s.forEach((function(t){h+='"})),h+='",h+="
ClienteRFCCorriente1-30 dias31-60 dias61-90 dias90+ diasTotal
'+t.name+''+(t.rfc||"--")+'$'+a(t.corriente)+'$'+a(t.d1_30)+'0?' style="color:var(--color-warning)"':"")+">$"+a(t.d31_60)+'0?' style="color:var(--color-error)"':"")+">$"+a(t.d61_90)+'0?' style="color:var(--color-error);font-weight:700"':"")+">$"+a(t.d90_plus)+'$'+a(t.total)+"
TOTAL$'+a(i.corriente)+'$'+a(i.d1_30)+'$'+a(i.d31_60)+'$'+a(i.d61_90)+'$'+a(i.d90_plus)+'$'+a(i.total)+"
",n.innerHTML=s.length?h:r("No hay saldos pendientes de credito")}catch(a){t.innerHTML=o("Error cargando datos de clientes: "+a.message),n.innerHTML=""}}async function p(){d.financieros=!0;var t=document.getElementById("fin-month"),e=document.getElementById("fin-year"),l=parseInt(e.value),s=parseInt(t.value),i=document.getElementById("financieros-kpis"),h=document.getElementById("financieros-income"),v=document.getElementById("financieros-balance"),m=document.getElementById("financieros-trial"),p=document.getElementById("financieros-cortes");i.innerHTML='
Cargando...
',h.innerHTML='
Cargando...
',v.innerHTML='
Cargando...
',m.innerHTML='
Cargando...
',p.innerHTML='
Cargando...
';try{var[u,y,b,f]=await Promise.all([c("/pos/api/accounting/income-statement?year="+l+"&month="+s),c("/pos/api/accounting/balance-sheet"),c("/pos/api/accounting/trial-balance?year="+l+"&month="+s),c("/pos/api/register/history?per_page=50")]);i.innerHTML=g("Ingresos","$"+a(u.ingresos.total),"periodo "+s+"/"+l)+g("Costos","$"+a(u.costos.total),"")+g("Utilidad Bruta","$"+a(u.utilidad_bruta),"")+g("Utilidad Neta","$"+a(u.utilidad_neta),u.ingresos.total>0?"Margen: "+(u.utilidad_neta/u.ingresos.total*100).toFixed(1)+"%":"");var _='
Estado de Resultados'+s+"/"+l+"
";_+='
',_+='',(u.ingresos.items||[]).forEach((function(t){_+='"})),_+='",_+='',(u.costos.items||[]).forEach((function(t){_+='"})),_+='",_+='",_+='',(u.gastos.items||[]).forEach((function(t){_+='"})),_+='";var T=u.utilidad_neta>=0?"var(--color-success)":"var(--color-error)";_+='",_+="
CuentaCodigoMonto
INGRESOS
'+t.name+''+t.code+'$'+a(t.amount)+"
Total Ingresos$'+a(u.ingresos.total)+"
COSTOS
'+t.name+''+t.code+'$'+a(t.amount)+"
Total Costos$'+a(u.costos.total)+"
UTILIDAD BRUTA$'+a(u.utilidad_bruta)+"
GASTOS
'+t.name+''+t.code+'$'+a(t.amount)+"
Total Gastos$'+a(u.gastos.total)+"
UTILIDAD NETA$'+a(u.utilidad_neta)+"
",h.innerHTML=_;var M='
Balance General'+(y.balanced?"Cuadrado":"Descuadrado")+"
";M+='
',M+='',(y.activo.items||[]).forEach((function(t){M+='"})),M+='",M+='',(y.pasivo.items||[]).forEach((function(t){M+='"})),M+='",M+='',(y.capital.items||[]).forEach((function(t){M+='"})),M+='",M+='",M+="
CuentaCodigoSaldo
ACTIVO
'+t.name+''+t.code+'$'+a(t.balance)+"
Total Activo$'+a(y.activo.total)+"
PASIVO
'+t.name+''+t.code+'$'+a(t.balance)+"
Total Pasivo$'+a(y.pasivo.total)+"
CAPITAL
'+t.name+''+t.code+'$'+a(t.balance)+"
Total Capital$'+a(y.capital.total)+"
Pasivo + Capital$'+a(y.pasivo.total+y.capital.total)+"
",v.innerHTML=M;var x=b.data||[],L='
Balanza de Comprobacion'+s+"/"+l+"
";L+='
',x.forEach((function(t){L+='"})),L+="
CodigoCuentaTipoSaldo InicialCargosAbonosSaldo Final
'+t.code+''+t.name+''+t.type+'$'+a(t.saldo_inicial)+'$'+a(t.cargos)+'$'+a(t.abonos)+'$'+a(t.saldo_final)+"
",m.innerHTML=x.length?L:r("Sin movimientos contables en este periodo");var E=f.data||[],C='
Cortes de Caja'+(f.pagination?f.pagination.total:E.length)+" cortes
";C+='
',E.forEach((function(t){var e=t.difference<0?"color:var(--color-error)":t.difference>0?"color:var(--color-warning)":"color:var(--color-success)";C+='"})),C+="
CajaEmpleadoAperturaCierreMonto AperturaEsperadoCierre RealDiferencia
#'+t.register_number+''+(t.employee_name||"--")+''+n(t.opened_at)+''+n(t.closed_at)+'$'+a(t.opening_amount)+'$'+a(t.expected_amount)+'$'+a(t.closing_amount)+'$'+a(t.difference)+"
",p.innerHTML=E.length?C:r("No hay cortes de caja registrados")}catch(t){i.innerHTML=o("Error cargando reportes financieros: "+t.message),h.innerHTML="",v.innerHTML="",m.innerHTML="",p.innerHTML=""}}function u(){if(t()||(window.location.href="/pos/login",0)){try{l(localStorage.getItem("nexus-theme")||"industrial")}catch(t){}i(),setInterval(i,1e3);var a=new Date,e=new Date(a.getFullYear(),a.getMonth(),1),n=document.getElementById("ventas-date-from"),r=document.getElementById("ventas-date-to");n&&(n.value=e.toISOString().substring(0,10)),r&&(r.value=a.toISOString().substring(0,10));var o=document.getElementById("fin-month"),d=document.getElementById("fin-year");if(o)for(var s=["Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"],c=0;c<12;c++){(v=document.createElement("option")).value=c+1,v.textContent=s[c],c===a.getMonth()&&(v.selected=!0),o.appendChild(v)}if(d)for(var g=a.getFullYear();g>=a.getFullYear()-3;g--){var v;(v=document.createElement("option")).value=g,v.textContent=g,d.appendChild(v)}h()}}return window.setTheme=l,window.switchTab=s,document.addEventListener("DOMContentLoaded",u),{init:u,setTheme:l,switchTab:s,loadVentas:h,loadInventario:v,loadClientes:m,loadFinancieros:p,fmt:a}})();function exportReportCSV(){for(var t=document.querySelectorAll("table"),a=null,e=0;e'},{name:e("pos"),href:"/pos/sale",icon:''},{name:e("catalog"),href:"/pos/catalog",icon:''},{name:e("inventory"),href:"/pos/inventory",icon:''}]},{label:e("nav_management"),items:[{name:e("customers"),href:"/pos/customers",icon:''},{name:"Cotizaciones",href:"/pos/quotations",icon:''},{name:"Marketplace",href:"/pos/marketplace",icon:''},{name:e("invoicing"),href:"/pos/invoicing",icon:''},{name:e("accounting"),href:"/pos/accounting",icon:''},{name:e("reports"),href:"/pos/reports",icon:''},{name:e("fleet"),href:"/pos/fleet",icon:''},{name:e("whatsapp"),href:"/pos/whatsapp",icon:''}]},{label:e("nav_system"),items:[{name:e("config"),href:"/pos/config",icon:''}]}];var d="";l.forEach((function(e){d+='",e.items.forEach((function(e){var r=i===e.href;d+='"+(''+e.icon+"")+e.name+""}))}));var c='',p='';window.updateThemeButtons=function(){var e=localStorage.getItem("pos_theme")||"industrial";document.querySelectorAll(".theme-toggle-btn").forEach((function(r,a){r.classList.toggle("is-active",0===a?"industrial"===e:"modern"===e)}))};var g='"+c+p+'',m=document.createElement("style");m.textContent=[".pos-sidebar{position:fixed;top:0;left:0;bottom:0;width:260px;display:flex;flex-direction:column;background:var(--color-bg-elevated);border-right:1px solid var(--color-border);z-index:100;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--scrollbar-thumb,#444) var(--scrollbar-track,#222);font-family:var(--font-body)}",".pos-sidebar::-webkit-scrollbar{width:4px}",".pos-sidebar::-webkit-scrollbar-track{background:var(--scrollbar-track,#222)}",".pos-sidebar::-webkit-scrollbar-thumb{background:var(--scrollbar-thumb,#444);border-radius:99px}",".sidebar__brand{display:flex;align-items:center;gap:var(--space-3,12px);padding:var(--space-4,16px) var(--space-4,16px) var(--space-3,12px);border-bottom:1px solid var(--color-border);flex-shrink:0}",".brand-logo{width:36px;height:36px;display:flex;align-items:center;justify-content:center;background:var(--color-primary);color:var(--color-text-inverse,#fff);font-family:var(--font-heading);font-weight:800;font-size:1rem;letter-spacing:-0.04em;flex-shrink:0}",'[data-theme="industrial"] .brand-logo{clip-path:polygon(0 0,calc(100% - 9px) 0,100% 9px,100% 100%,0 100%)}','[data-theme="modern"] .brand-logo{border-radius:var(--radius-md,8px)}',".brand-name__primary{font-family:var(--font-heading);font-weight:800;font-size:0.9375rem;letter-spacing:var(--tracking-wide,0.02em);text-transform:uppercase;color:var(--color-text-primary);line-height:1}",".brand-name__sub{font-size:var(--text-caption,0.75rem);color:var(--color-text-muted);letter-spacing:var(--tracking-wider,0.04em);text-transform:uppercase;margin-top:2px}",".sidebar__nav{flex:1;padding:var(--space-3,12px) 0}",".nav-section-label{padding:var(--space-3,12px) var(--space-4,16px) var(--space-1,4px);font-size:0.6875rem;font-weight:600;letter-spacing:var(--tracking-widest,0.08em);text-transform:uppercase;color:var(--color-text-muted)}",".nav-item{display:flex;align-items:center;gap:var(--space-3,12px);padding:var(--space-2,8px) var(--space-4,16px);color:var(--color-text-secondary);text-decoration:none;font-size:var(--text-body-sm,0.875rem);font-weight:400;border-left:3px solid transparent;transition:all 0.15s;cursor:pointer}",".nav-item:hover{background:var(--color-surface-2,rgba(255,255,255,0.04));color:var(--color-text-primary)}",".nav-item.is-active{background:var(--color-primary-muted,rgba(245,166,35,0.12));color:var(--color-primary);border-left-color:var(--color-primary);font-weight:600}",".nav-item__icon{width:18px;height:18px;flex-shrink:0;opacity:0.7}",".nav-item.is-active .nav-item__icon{opacity:1}",".sidebar__theme-toggle,.sidebar__lang-toggle{display:flex;gap:4px;padding:8px 16px;border-top:1px solid var(--color-border)}",".theme-toggle-btn,.lang-toggle-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:6px;border:1px solid var(--color-border);border-radius:var(--radius-sm,4px);background:none;color:var(--color-text-muted);cursor:pointer;transition:all 0.15s;font-size:0.75rem}",".theme-toggle-btn:hover,.lang-toggle-btn:hover{color:var(--color-text-primary);background:var(--color-surface-2,rgba(255,255,255,0.04))}",".theme-toggle-btn.is-active,.lang-toggle-btn.is-active{background:var(--color-primary-muted,rgba(245,166,35,0.12));color:var(--color-primary);border-color:var(--color-primary)}",".lang-flag{font-weight:700;font-size:0.625rem;letter-spacing:0.04em}",".sidebar__footer{padding:var(--space-3,12px) var(--space-4,16px);border-top:1px solid var(--color-border);display:flex;align-items:center;gap:var(--space-2,8px)}",".sidebar__user-avatar{width:28px;height:28px;border-radius:50%;background:var(--color-primary);color:var(--color-text-inverse,#fff);display:flex;align-items:center;justify-content:center;font-size:0.6875rem;font-weight:700;flex-shrink:0}",".sidebar__user-info{flex:1;overflow:hidden}",".sidebar__user-name{font-size:var(--text-body-sm,0.875rem);font-weight:600;color:var(--color-text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",".sidebar__user-role{font-size:var(--text-caption,0.75rem);color:var(--color-text-muted)}",".sidebar__logout-btn{background:none;border:1px solid var(--color-border);border-radius:var(--radius-sm,4px);padding:4px 6px;cursor:pointer;color:var(--color-text-muted);transition:all 0.15s;display:flex;align-items:center}",".sidebar__logout-btn:hover{color:var(--color-error,#F85149);border-color:var(--color-error,#F85149)}",".pos-main-offset{margin-left:260px}","@media(max-width:768px){.pos-sidebar{width:56px}.brand-name,.nav-item span,.sidebar__user-info,.nav-section-label,.sidebar__theme-toggle,.sidebar__lang-toggle{display:none}.sidebar__brand{justify-content:center;padding:12px 8px}.sidebar__footer{flex-direction:column;padding:8px}.pos-main-offset{margin-left:56px}}"].join("\n"),document.head.appendChild(m);var v=document.querySelector("aside.sidebar, .sidebar, #sidebar");if(v)v.className="pos-sidebar",v.innerHTML=g,v.removeAttribute("style");else{var b=document.createElement("aside");b.className="pos-sidebar",b.innerHTML=g,document.body.insertBefore(b,document.body.firstChild)}var x=document.querySelector("main, .main-content, #mainContent, .main, .page-content");x&&x.classList.add("pos-main-offset");var h=document.querySelector(".pos-sidebar, .sidebar, #sidebar"),u=document.getElementById("sidebar-overlay");!u&&h&&((u=document.createElement("div")).id="sidebar-overlay",u.className="sidebar-overlay",u.addEventListener("click",(function(){_()})),h.parentNode.insertBefore(u,h));var f=document.getElementById("hamburger-btn");function y(){if(h){var e=h.classList.contains("open");h.classList.toggle("open",!e),u&&u.classList.toggle("open",!e),document.body.style.overflow=e?"":"hidden"}}function _(){h&&h.classList.remove("open"),u&&u.classList.remove("open"),document.body.style.overflow=""}f||((f=document.createElement("button")).id="hamburger-btn",f.className="hamburger-btn",f.setAttribute("aria-label","Menú"),f.innerHTML='',f.style.cssText="display:none;position:fixed;top:10px;left:10px;z-index:"+(parseInt(getComputedStyle(document.documentElement).getPropertyValue("--z-modal")||1050)+2)+";background:var(--glass-bg-strong);backdrop-filter:blur(12px);border:1px solid var(--glass-border);border-radius:var(--radius-md);padding:8px;cursor:pointer;color:var(--color-text-primary);box-shadow:0 2px 8px rgba(0,0,0,0.2);",f.addEventListener("click",(function(){y()})),document.body.appendChild(f)),window.addEventListener("resize",(function(){window.innerWidth>=1024&&_()})),window.toggleSidebar=y,window.closeSidebar=_}(); \ No newline at end of file diff --git a/pos/static/js/sync-engine.min.js b/pos/static/js/sync-engine.min.js new file mode 100644 index 0000000..83895f1 --- /dev/null +++ b/pos/static/js/sync-engine.min.js @@ -0,0 +1 @@ +!function(){"use strict";var n="sync_queue",e="inventory_cache",t="cached_parts",r=null;function o(){return new Promise((function(o,c){if(r)o(r);else{var i=indexedDB.open("nexus_pos_offline",2);i.onupgradeneeded=function(r){var o=r.target.result;if(o.objectStoreNames.contains(n)||o.createObjectStore(n,{keyPath:"id",autoIncrement:!0}),!o.objectStoreNames.contains(e)){var c=o.createObjectStore(e,{keyPath:"item_id"});c.createIndex("sku","sku",{unique:!1}),c.createIndex("name","name",{unique:!1})}if(!o.objectStoreNames.contains(t)){var i=o.createObjectStore(t,{keyPath:"part_number"});i.createIndex("name","name",{unique:!1}),i.createIndex("category","category",{unique:!1})}},i.onsuccess=function(n){r=n.target.result,o(r)},i.onerror=function(){c(i.error)}}}))}function c(){return o().then((function(e){return new Promise((function(t,r){var o=e.transaction(n,"readonly").objectStore(n).getAll();o.onsuccess=function(){t(o.result)},o.onerror=function(){r(o.error)}}))})).then((function(e){if(!e.length)return Promise.resolve({synced:0});var t=Promise.resolve(),r=0,c=0;return e.forEach((function(e){t=t.then((function(){var t={method:e.method,headers:{"Content-Type":"application/json"}};return e.body&&(t.body=JSON.stringify(e.body)),fetch(e.url,t).then((function(t){if(t.ok)return r++,i=e.id,o().then((function(e){return new Promise((function(t,r){var o=e.transaction(n,"readwrite");o.objectStore(n).delete(i),o.oncomplete=function(){t()},o.onerror=function(){r(o.error)}}))}));var i;c++})).catch((function(){c++}))}))})),t.then((function(){return{synced:r,failed:c,total:e.length}}))}))}window.addEventListener("online",(function(){console.log("[SyncEngine] Online — processing queue..."),c().then((function(n){n.synced>0&&console.log("[SyncEngine] Synced "+n.synced+" operations")})).catch((function(n){console.error("[SyncEngine] Queue processing error:",n)}))})),window.addEventListener("offline",(function(){console.log("[SyncEngine] Offline — operations will be queued")})),"serviceWorker"in navigator&&navigator.serviceWorker.addEventListener("message",(function(n){n.data&&"SYNC_REQUESTED"===n.data.type&&c()})),o().catch((function(n){console.error("[SyncEngine] Failed to open IndexedDB:",n)})),window.SyncEngine={queueOperation:function(e,t,r){return o().then((function(o){return new Promise((function(c,i){var a=o.transaction(n,"readwrite");a.objectStore(n).add({url:e,method:t,body:r||null,timestamp:Date.now()}),a.oncomplete=function(){c()},a.onerror=function(){i(a.error)}}))}))},processQueue:c,getQueueCount:function(){return o().then((function(e){return new Promise((function(t,r){var o=e.transaction(n,"readonly").objectStore(n).count();o.onsuccess=function(){t(o.result)},o.onerror=function(){r(o.error)}}))}))},isOnline:function(){return navigator.onLine},cacheInventory:function(){return fetch("/pos/api/sync/inventory").then((function(n){if(!n.ok)throw new Error("Sync inventory failed: "+n.status);return n.json()})).then((function(n){var t=n.items||[];return o().then((function(n){return new Promise((function(r,o){var c=n.transaction(e,"readwrite"),i=c.objectStore(e);i.clear(),t.forEach((function(n){i.put(n)})),c.oncomplete=function(){r(t.length)},c.onerror=function(){o(c.error)}}))}))}))},getCachedInventory:function(n){return o().then((function(t){return new Promise((function(r,o){var c=t.transaction(e,"readonly").objectStore(e).getAll();c.onsuccess=function(){var e=c.result;if(n){var t=n.toLowerCase(),o=e.filter((function(n){return n.sku&&-1!==n.sku.toLowerCase().indexOf(t)||n.name&&-1!==n.name.toLowerCase().indexOf(t)||n.barcode&&-1!==n.barcode.toLowerCase().indexOf(t)}));r(o)}else r(e)},c.onerror=function(){o(c.error)}}))}))},cacheTopParts:function(){return fetch("/pos/api/sync/top-parts").then((function(n){if(!n.ok)throw new Error("Sync top-parts failed: "+n.status);return n.json()})).then((function(n){var e=n.parts||[];return o().then((function(n){return new Promise((function(r,o){var c=n.transaction(t,"readwrite"),i=c.objectStore(t);i.clear(),e.forEach((function(n){i.put(n)})),c.oncomplete=function(){console.log("[SyncEngine] Cached "+e.length+" top parts"),r(e.length)},c.onerror=function(){o(c.error)}}))}))}))},searchCachedParts:function(n){return o().then((function(e){return new Promise((function(r,o){var c=e.transaction(t,"readonly").objectStore(t).getAll();c.onsuccess=function(){var e=c.result;if(n){var t=n.toLowerCase(),o=e.filter((function(n){return n.part_number&&-1!==n.part_number.toLowerCase().indexOf(t)||n.name&&-1!==n.name.toLowerCase().indexOf(t)||n.category&&-1!==n.category.toLowerCase().indexOf(t)||n.brand&&-1!==n.brand.toLowerCase().indexOf(t)}));r(o)}else r(e)},c.onerror=function(){o(c.error)}}))}))}}}(); \ No newline at end of file diff --git a/pos/static/js/whatsapp.min.js b/pos/static/js/whatsapp.min.js new file mode 100644 index 0000000..effb19d --- /dev/null +++ b/pos/static/js/whatsapp.min.js @@ -0,0 +1 @@ +!function(){"use strict";var e=localStorage.getItem("pos_token");if(e){var t=null,n=null,o=null,a=document.getElementById("convList"),s=document.getElementById("chatMessages"),i=document.getElementById("chatHeaderPhone"),r=document.getElementById("chatInput"),c=document.getElementById("sendBtn"),l=document.getElementById("newChatBtn"),d=document.getElementById("emptyState"),u=document.getElementById("chatPanel"),p=document.getElementById("statusDot"),v=document.getElementById("statusText"),m=document.getElementById("connectSection"),h=document.getElementById("messengerArea"),y=document.getElementById("qrImg"),f=document.getElementById("qrPlaceholder"),g=document.getElementById("connectBtn"),E=document.getElementById("disconnectBtn"),b=document.getElementById("refreshQrBtn");g.addEventListener("click",(function(){g.disabled=!0,g.textContent="Creando instancia...",B("POST","/connect").then((function(e){g.disabled=!1,g.textContent="Conectar WhatsApp",e.error?alert("Error: "+(e.error.message||e.error)):w()})).catch((function(){g.disabled=!1,g.textContent="Conectar WhatsApp",alert("Error de red al crear instancia")}))})),E.addEventListener("click",(function(){confirm("Desconectar WhatsApp?")&&B("POST","/logout").then((function(){k("close"),q()}))})),b.addEventListener("click",w),window.deleteAllConversations=function(){confirm("Borrar TODAS las conversaciones? Esta accion no se puede deshacer.")&&B("DELETE","/conversations").then((function(e){e.ok&&(t=null,u.style.display="none",d.style.display="",D())}))};var x="";c.addEventListener("click",P),r.addEventListener("keydown",(function(e){"Enter"!==e.key||e.shiftKey||(e.preventDefault(),P())})),l.addEventListener("click",(function(){var e=prompt("Numero de telefono (formato: 5215512345678):");e&&(O(e=e.replace(/[\s\-\+\(\)]/g,"")),D())}));var I=document.getElementById("sendQuoteBtn");I&&I.addEventListener("click",(function(){t?fetch("/pos/api/quotations?per_page=20",{headers:C()}).then((function(e){return e.json()})).then((function(e){var n=(e.data||[]).filter((function(e){return"active"===e.status}));if(0!==n.length){var o="Cotizaciones activas:\n";n.forEach((function(e){o+="#"+e.id+" — $"+e.total.toFixed(2)+" ("+(e.customer_name||e.source||"sin cliente")+")\n"}));var a=prompt(o+"\nEscribe el ID de la cotizacion a enviar:");a&&fetch("/pos/api/quotations/"+a,{headers:C()}).then((function(e){return e.json()})).then((function(e){if(e.error)alert("Error: "+e.error);else{var n=["📄 *COTIZACIÓN #"+e.id+"*",""];(e.items||[]).forEach((function(e,t){n.push(t+1+". "+e.name),n.push(" #"+e.part_number+" × "+e.quantity+" = $"+e.subtotal.toFixed(2))})),n.push("─────────────"),n.push("Subtotal: $"+e.subtotal.toFixed(2)),n.push("IVA: $"+e.tax_total.toFixed(2)),n.push("*TOTAL: $"+e.total.toFixed(2)+"*");var o=n.join("\n");B("POST","/send",{phone:t,message:o}).then((function(e){e.error?alert("Error enviando: "+e.error):(N(t),D())}))}}))}else alert("No hay cotizaciones activas. Crea una desde el POS (F4) o via WhatsApp.")})):alert("Selecciona una conversacion primero")})),A(),setInterval(A,3e4);try{var _=JSON.parse(atob(e.split(".")[1]));window.POS_USER={name:_.name||"Usuario",roleLabel:(_.role||"").charAt(0).toUpperCase()+(_.role||"").slice(1),initials:(_.name||"U").split(" ").map((function(e){return e[0]})).join("").slice(0,2).toUpperCase()}}catch(e){}}else window.location.href="/pos/login";function C(){return{Authorization:"Bearer "+e,"Content-Type":"application/json"}}function B(e,t,n){var o={method:e,headers:C()};return n&&(o.body=JSON.stringify(n)),fetch("/pos/api/whatsapp"+t,o).then((function(e){return 401===e.status&&(window.location.href="/pos/login"),e.json()}))}function S(e){var t=document.createElement("div");return t.textContent=e||"",t.innerHTML}function T(e){if(!e)return"";var t=new Date(e),n=new Date;return t.toDateString()===n.toDateString()?t.toLocaleTimeString("es-MX",{hour:"2-digit",minute:"2-digit"}):t.toLocaleDateString("es-MX",{day:"2-digit",month:"short"})+" "+t.toLocaleTimeString("es-MX",{hour:"2-digit",minute:"2-digit"})}function L(e){return e?13===e.length&&e.startsWith("521")?"+52 1 "+e.slice(3,5)+" "+e.slice(5,9)+" "+e.slice(9):12===e.length&&e.startsWith("52")?"+52 "+e.slice(2,4)+" "+e.slice(4,8)+" "+e.slice(8):"+"+e:""}function A(){B("GET","/status").then((function(e){k((e.instance||e).state||e.state||"close")})).catch((function(){k("close")}))}function k(e){"open"===e?(p.className="status-dot status-dot--ok",v.textContent="Conectado",m.style.display="none",h.style.display="flex",E.style.display="",g.style.display="none",D(),M()):"connecting"===e?(p.className="status-dot status-dot--warn",v.textContent="Escaneando QR...",m.style.display="flex",h.style.display="none",E.style.display="none",g.style.display="none",b.style.display=""):(p.className="status-dot status-dot--error",v.textContent="Desconectado",m.style.display="flex",h.style.display="none",E.style.display="none",g.style.display="",b.style.display="none",y.style.display="none",f.style.display="")}function w(){f.textContent="Generando QR...",B("GET","/qr").then((function(e){var t=e.qr||e.base64||e.qrcode||"";t?(y.src=t.startsWith("data:")?t:"data:image/png;base64,"+t,y.style.display="block",f.style.display="none",b.style.display="",k("connecting"),q(),o=setInterval((function(){B("GET","/status").then((function(e){"open"===((e.instance||e).state||e.state||"close")&&(k("open"),q(),D(),M())}))}),3e3)):e.instance&&"open"===e.instance.state||"open"===e.state?(k("open"),D()):(f.textContent="No se pudo generar el QR. Intenta de nuevo.",f.style.display="",y.style.display="none")})).catch((function(){f.textContent="Error al obtener QR"}))}function q(){o&&(clearInterval(o),o=null)}function D(){B("GET","/conversations").then((function(e){var n=e.conversations||[];if(0!==n.length){var o="";n.forEach((function(e){var n=e.phone===t,a="outgoing"===e.last_direction?"↗ ":"↙ ",s=e.contact_name||"";s||(s=e.phone.length>13||!/^(52|1|44|34)/.test(e.phone)?"Contacto WhatsApp":L(e.phone));o+='
'+S(s)+'
'+a+S(e.last_message||"(sin texto)")+'
'+T(e.last_at)+'
'})),o+='
',a.innerHTML=o,a.querySelectorAll(".conv-item").forEach((function(e){e.addEventListener("click",(function(t){if(!t.target.classList.contains("conv-item__delete")){var n=e.querySelector(".conv-item__phone")?e.querySelector(".conv-item__phone").textContent:"";O(e.getAttribute("data-phone"),n)}}))})),a.querySelectorAll(".conv-item__delete").forEach((function(e){e.addEventListener("click",(function(n){n.stopPropagation();var o=e.getAttribute("data-del-phone");confirm("Borrar conversacion con "+L(o)+"?")&&function(e){B("DELETE","/conversations/"+encodeURIComponent(e)).then((function(n){n.ok?(t===e&&(t=null,u.style.display="none",d.style.display=""),D()):alert("Error: "+(n.error||"unknown"))}))}(o)}))}))}else a.innerHTML='
No hay conversaciones
'})).catch((function(){a.innerHTML='
Error cargando conversaciones
'}))}function O(e,n){t=e;var o=e.length>13||!/^(52|1|44|34)/.test(e);x=n||"",i.textContent=x||(o?"Contacto WhatsApp":L(e)),d.style.display="none",u.style.display="flex",a.querySelectorAll(".conv-item").forEach((function(t){t.classList.toggle("is-active",t.getAttribute("data-phone")===e)})),N(e),M()}function N(e){B("GET","/conversations/"+encodeURIComponent(e)).then((function(e){!function(e){var t="";e.forEach((function(e){var n="outgoing"===e.direction?"msg-bubble--out":"msg-bubble--in",o=e.message_text||e.text||"",a=e.created_at||e.date||"";t+='
'+S(o).replace(/\n/g,"
")+'
'+T(a)+"
"})),s.innerHTML=t||'
Sin mensajes
',s.scrollTop=s.scrollHeight}(e.messages||[])}))}function P(){var e=r.value.trim();e&&t&&(r.value="",c.disabled=!0,B("POST","/send",{phone:t,message:e}).then((function(e){c.disabled=!1,e.error?alert("Error: "+e.error):(N(t),D())})).catch((function(){c.disabled=!1,alert("Error de red al enviar mensaje")})))}function M(){n&&clearInterval(n),n=setInterval((function(){t&&N(t),D()}),1e4)}}(); \ No newline at end of file diff --git a/scripts/minify-assets.sh b/scripts/minify-assets.sh new file mode 100755 index 0000000..d1d3c1e --- /dev/null +++ b/scripts/minify-assets.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# Minify JS and CSS assets for production deploy +# Run this before restarting nginx / gunicorn + +set -e + +echo "=== Minifying JS assets ===" +for f in /home/Autopartes/pos/static/js/*.js; do + base=$(basename "$f" .js) + out="/home/Autopartes/pos/static/js/${base}.min.js" + echo " $base.js -> ${base}.min.js" + terser "$f" -o "$out" --compress --mangle 2>/dev/null || cp "$f" "$out" +done + +echo "=== Minifying CSS assets ===" +for f in /home/Autopartes/pos/static/css/*.css; do + base=$(basename "$f" .css) + out="/home/Autopartes/pos/static/css/${base}.min.css" + echo " $base.css -> ${base}.min.css" + if command -v csso >/dev/null 2>&1; then + csso "$f" -o "$out" 2>/dev/null || cp "$f" "$out" + else + cp "$f" "$out" + fi +done + +echo "=== Minifying Dashboard assets ===" +for f in /home/Autopartes/dashboard/*.js; do + base=$(basename "$f" .js) + out="/home/Autopartes/dashboard/${base}.min.js" + echo " $base.js -> ${base}.min.js" + terser "$f" -o "$out" --compress --mangle 2>/dev/null || cp "$f" "$out" +done + +for f in /home/Autopartes/dashboard/*.css; do + base=$(basename "$f" .css) + out="/home/Autopartes/dashboard/${base}.min.css" + echo " $base.css -> ${base}.min.css" + if command -v csso >/dev/null 2>&1; then + csso "$f" -o "$out" 2>/dev/null || cp "$f" "$out" + else + cp "$f" "$out" + fi +done + +echo "=== Done ===" +echo "Minified files ready. Nginx will serve .min.js/.min.css when available." diff --git a/scripts/warm_vehicle_cache.py b/scripts/warm_vehicle_cache.py new file mode 100755 index 0000000..c1aad7f --- /dev/null +++ b/scripts/warm_vehicle_cache.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Warm Redis cache for vehicle info (part_vehicle_preview alternative). + +Runs in batches over all parts in the catalog, populating +nexus:vehicle:{part_id} keys in Redis. This eliminates the +DISTINCT ON + 4 JOINs query on vehicle_parts (2B rows) for +cached parts. + +Usage: + export MASTER_DB_URL="postgresql://..." + export REDIS_URL="redis://localhost:6379/0" + python3 warm_vehicle_cache.py +""" + +import os, sys, json, time +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'pos')) + +import psycopg2 +import redis + +def _fix_dsn(dsn): + if dsn and 'host=' not in dsn and '@/' in dsn: + dsn = dsn.replace('@/', '@localhost/') + return dsn + + +MASTER_DB_URL = _fix_dsn(os.environ.get('MASTER_DB_URL', 'postgresql://postgres@localhost/nexus_autoparts')) +REDIS_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379/0') +BATCH_SIZE = 5000 +TTL_SECONDS = 3600 + + +def main(): + print("Connecting to master DB and Redis...") + conn = psycopg2.connect(MASTER_DB_URL) + cur = conn.cursor() + r = redis.from_url(REDIS_URL, decode_responses=True) + r.ping() + + # Get all part_ids + cur.execute("SELECT id_part FROM parts WHERE oem_part_number IS NOT NULL ORDER BY id_part") + all_ids = [r[0] for r in cur.fetchall()] + total = len(all_ids) + print(f"Total parts to warm: {total}") + + processed = 0 + cached = 0 + start = time.time() + + for i in range(0, total, BATCH_SIZE): + batch = all_ids[i:i + BATCH_SIZE] + cur.execute(""" + SELECT DISTINCT ON (vp.part_id) + vp.part_id, b.name_brand, m.name_model, y.year_car + FROM vehicle_parts vp + JOIN model_year_engine mye ON mye.id_mye = vp.model_year_engine_id + JOIN models m ON m.id_model = mye.model_id + JOIN brands b ON b.id_brand = m.brand_id + JOIN years y ON y.id_year = mye.year_id + WHERE vp.part_id = ANY(%s) + ORDER BY vp.part_id, y.year_car DESC + """, (batch,)) + + pipe = r.pipeline() + batch_cached = 0 + for row in cur.fetchall(): + info = f"{row[1]} {row[2]} {row[3]}" + pipe.setex(f'nexus:vehicle:{row[0]}', TTL_SECONDS, info) + batch_cached += 1 + pipe.execute() + + processed += len(batch) + cached += batch_cached + elapsed = time.time() - start + rate = processed / elapsed if elapsed > 0 else 0 + print(f" [{processed}/{total}] cached={batch_cached} ({rate:.0f}/s)") + + cur.close() + conn.close() + print(f"\nDone. Cached {cached} vehicle entries in {elapsed:.0f}s") + + +if __name__ == '__main__': + main()