feat: add captura, POS, cuentas, and tienda pages
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
660
dashboard/captura.css
Normal file
660
dashboard/captura.css
Normal file
@@ -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);
|
||||||
|
}
|
||||||
99
dashboard/captura.html
Normal file
99
dashboard/captura.html
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Captura de Datos — NEXUS AUTOPARTS</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Orbitron:wght@700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="/shared.css">
|
||||||
|
<link rel="stylesheet" href="/captura.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="shared-nav"></div>
|
||||||
|
|
||||||
|
<div class="captura-container">
|
||||||
|
<!-- Main Tabs -->
|
||||||
|
<div class="captura-tabs">
|
||||||
|
<button class="captura-tab active" data-tab="oem">Partes OEM</button>
|
||||||
|
<button class="captura-tab" data-tab="aftermarket">Intercambios</button>
|
||||||
|
<button class="captura-tab" data-tab="images">Imagenes</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============================================ -->
|
||||||
|
<!-- SECTION 1: OEM Parts Entry -->
|
||||||
|
<!-- ============================================ -->
|
||||||
|
<div id="section-oem" class="captura-section active">
|
||||||
|
<!-- Vehicle selection view -->
|
||||||
|
<div id="oem-vehicle-select">
|
||||||
|
<div class="status-tabs">
|
||||||
|
<button class="status-tab active" data-status="pending">Pendientes</button>
|
||||||
|
<button class="status-tab" data-status="in_progress">En progreso</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="vehicle-filters">
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Marca</label>
|
||||||
|
<select id="oem-brand-filter">
|
||||||
|
<option value="">Todas</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Modelo</label>
|
||||||
|
<input id="oem-model-filter" type="text" placeholder="Buscar modelo...">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="oem-vehicle-list" class="vehicle-list">
|
||||||
|
<div class="loading"><div class="spinner"></div></div>
|
||||||
|
</div>
|
||||||
|
<div id="oem-vehicle-pagination" class="captura-pagination"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Part entry view (hidden until vehicle selected) -->
|
||||||
|
<div id="oem-part-entry" style="display: none;">
|
||||||
|
<div id="oem-vehicle-header" class="vehicle-header"></div>
|
||||||
|
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||||
|
<div>
|
||||||
|
<div class="progress-bar" style="width: 200px;">
|
||||||
|
<div id="oem-progress-fill" class="progress-fill" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<span id="oem-progress-text" class="progress-text">0 partes registradas</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="oem-groups-container"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============================================ -->
|
||||||
|
<!-- SECTION 2: Aftermarket / Interchange Entry -->
|
||||||
|
<!-- ============================================ -->
|
||||||
|
<div id="section-aftermarket" class="captura-section">
|
||||||
|
<div class="captura-search">
|
||||||
|
<input id="aftermarket-search" type="text" placeholder="Buscar por # OEM o nombre...">
|
||||||
|
<button class="btn btn-primary" onclick="loadPartsWithoutAftermarket()">Buscar</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="aftermarket-list"></div>
|
||||||
|
<div id="aftermarket-pagination" class="captura-pagination"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============================================ -->
|
||||||
|
<!-- SECTION 3: Image Upload -->
|
||||||
|
<!-- ============================================ -->
|
||||||
|
<div id="section-images" class="captura-section">
|
||||||
|
<div class="captura-search">
|
||||||
|
<input id="image-search" type="text" placeholder="Buscar por # OEM o nombre...">
|
||||||
|
<button class="btn btn-primary" onclick="loadPartsWithoutImage()">Buscar</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="image-list"></div>
|
||||||
|
<div id="image-pagination" class="captura-pagination"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/nav.js"></script>
|
||||||
|
<script src="/captura.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
707
dashboard/captura.js
Normal file
707
dashboard/captura.js
Normal file
@@ -0,0 +1,707 @@
|
|||||||
|
/**
|
||||||
|
* captura.js — Data entry logic for Nexus Autoparts
|
||||||
|
* 3 sections: OEM Parts, Aftermarket/Interchange, Images
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var API = '';
|
||||||
|
var currentMye = null; // selected vehicle MYE id
|
||||||
|
var currentVehicle = null; // vehicle info object
|
||||||
|
var vehicleParts = []; // existing parts for current vehicle
|
||||||
|
var manufacturers = []; // cached manufacturer list
|
||||||
|
var vehicleStatus = 'pending';
|
||||||
|
var vehiclePage = 1;
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Utility
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
function toast(msg, type) {
|
||||||
|
var el = document.createElement('div');
|
||||||
|
el.className = 'toast ' + (type || 'success');
|
||||||
|
el.textContent = msg;
|
||||||
|
document.body.appendChild(el);
|
||||||
|
setTimeout(function () { el.remove(); }, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function api(path, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
return fetch(API + path, opts).then(function (r) {
|
||||||
|
if (!r.ok) return r.json().then(function (d) { throw new Error(d.error || 'Error'); });
|
||||||
|
return r.json();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(s) {
|
||||||
|
if (!s) return '';
|
||||||
|
var d = document.createElement('div');
|
||||||
|
d.textContent = s;
|
||||||
|
return d.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Tab Switching
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
document.querySelectorAll('.captura-tab').forEach(function (tab) {
|
||||||
|
tab.addEventListener('click', function () {
|
||||||
|
document.querySelectorAll('.captura-tab').forEach(function (t) { t.classList.remove('active'); });
|
||||||
|
document.querySelectorAll('.captura-section').forEach(function (s) { s.classList.remove('active'); });
|
||||||
|
tab.classList.add('active');
|
||||||
|
var target = tab.getAttribute('data-tab');
|
||||||
|
document.getElementById('section-' + target).classList.add('active');
|
||||||
|
|
||||||
|
if (target === 'aftermarket') loadPartsWithoutAftermarket();
|
||||||
|
if (target === 'images') loadPartsWithoutImage();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// SECTION 1: OEM Parts
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
// --- Status tabs ---
|
||||||
|
document.querySelectorAll('.status-tab').forEach(function (tab) {
|
||||||
|
tab.addEventListener('click', function () {
|
||||||
|
document.querySelectorAll('.status-tab').forEach(function (t) { t.classList.remove('active'); });
|
||||||
|
tab.classList.add('active');
|
||||||
|
vehicleStatus = tab.getAttribute('data-status');
|
||||||
|
vehiclePage = 1;
|
||||||
|
loadVehicles();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Brand filter ---
|
||||||
|
function loadBrands() {
|
||||||
|
api('/api/brands').then(function (brands) {
|
||||||
|
var sel = document.getElementById('oem-brand-filter');
|
||||||
|
brands.forEach(function (b) {
|
||||||
|
var opt = document.createElement('option');
|
||||||
|
opt.value = b;
|
||||||
|
opt.textContent = b;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('oem-brand-filter').addEventListener('change', function () {
|
||||||
|
vehiclePage = 1;
|
||||||
|
loadVehicles();
|
||||||
|
});
|
||||||
|
|
||||||
|
var modelTimer = null;
|
||||||
|
document.getElementById('oem-model-filter').addEventListener('input', function () {
|
||||||
|
clearTimeout(modelTimer);
|
||||||
|
modelTimer = setTimeout(function () {
|
||||||
|
vehiclePage = 1;
|
||||||
|
loadVehicles();
|
||||||
|
}, 400);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Load vehicles ---
|
||||||
|
function loadVehicles() {
|
||||||
|
var brand = document.getElementById('oem-brand-filter').value;
|
||||||
|
var model = document.getElementById('oem-model-filter').value;
|
||||||
|
var list = document.getElementById('oem-vehicle-list');
|
||||||
|
list.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||||||
|
|
||||||
|
var endpoint = vehicleStatus === 'pending'
|
||||||
|
? '/api/captura/vehicles/pending'
|
||||||
|
: '/api/captura/vehicles/in-progress';
|
||||||
|
|
||||||
|
var params = '?page=' + vehiclePage + '&per_page=30';
|
||||||
|
if (brand) params += '&brand=' + encodeURIComponent(brand);
|
||||||
|
if (model) params += '&model=' + encodeURIComponent(model);
|
||||||
|
|
||||||
|
api(endpoint + params).then(function (res) {
|
||||||
|
var data = res.data || [];
|
||||||
|
if (data.length === 0) {
|
||||||
|
list.innerHTML = '<div class="empty-state"><div class="es-icon">📋</div><div class="es-text">No hay vehiculos ' +
|
||||||
|
(vehicleStatus === 'pending' ? 'pendientes' : 'en progreso') + '</div></div>';
|
||||||
|
document.getElementById('oem-vehicle-pagination').innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.innerHTML = data.map(function (v) {
|
||||||
|
return '<div class="vehicle-card" data-mye="' + v.id_mye + '">' +
|
||||||
|
'<div class="vc-brand">' + esc(v.brand) + '</div>' +
|
||||||
|
'<div class="vc-model">' + esc(v.model) + '</div>' +
|
||||||
|
'<div class="vc-details">' + v.year + ' · ' + esc(v.engine) +
|
||||||
|
(v.trim_level ? ' · ' + esc(v.trim_level) : '') + '</div>' +
|
||||||
|
(v.parts_count ? '<div class="vc-parts-count">' + v.parts_count + ' partes registradas</div>' : '') +
|
||||||
|
'</div>';
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// Click handler for vehicle cards
|
||||||
|
list.querySelectorAll('.vehicle-card').forEach(function (card) {
|
||||||
|
card.addEventListener('click', function () {
|
||||||
|
selectVehicle(parseInt(card.getAttribute('data-mye')));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
renderPagination('oem-vehicle-pagination', res.pagination, function (p) {
|
||||||
|
vehiclePage = p;
|
||||||
|
loadVehicles();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPagination(containerId, pag, onPage) {
|
||||||
|
var c = document.getElementById(containerId);
|
||||||
|
if (!pag || pag.total_pages <= 1) { c.innerHTML = ''; return; }
|
||||||
|
c.innerHTML = '<button ' + (pag.page <= 1 ? 'disabled' : '') + ' data-p="' + (pag.page - 1) + '">« Anterior</button>' +
|
||||||
|
'<span class="page-info">Pag ' + pag.page + ' de ' + pag.total_pages + ' (' + pag.total + ' total)</span>' +
|
||||||
|
'<button ' + (pag.page >= pag.total_pages ? 'disabled' : '') + ' data-p="' + (pag.page + 1) + '">Siguiente »</button>';
|
||||||
|
c.querySelectorAll('button').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
onPage(parseInt(btn.getAttribute('data-p')));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Select vehicle and show part entry ---
|
||||||
|
function selectVehicle(myeId) {
|
||||||
|
currentMye = myeId;
|
||||||
|
document.getElementById('oem-vehicle-select').style.display = 'none';
|
||||||
|
document.getElementById('oem-part-entry').style.display = 'block';
|
||||||
|
|
||||||
|
// Mark as in_progress
|
||||||
|
api('/api/captura/vehicles/' + myeId + '/status', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ status: 'in_progress' })
|
||||||
|
});
|
||||||
|
|
||||||
|
loadVehicleParts(myeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadVehicleParts(myeId) {
|
||||||
|
api('/api/captura/vehicles/' + myeId + '/parts').then(function (res) {
|
||||||
|
currentVehicle = res.vehicle;
|
||||||
|
vehicleParts = res.parts || [];
|
||||||
|
|
||||||
|
// Render vehicle header
|
||||||
|
var hdr = document.getElementById('oem-vehicle-header');
|
||||||
|
hdr.innerHTML = '<div class="vh-info">' +
|
||||||
|
'<div><div class="vh-label">Marca</div><div class="vh-value vh-brand">' + esc(currentVehicle.brand) + '</div></div>' +
|
||||||
|
'<div><div class="vh-label">Modelo</div><div class="vh-value">' + esc(currentVehicle.model) + '</div></div>' +
|
||||||
|
'<div><div class="vh-label">Ano</div><div class="vh-value">' + currentVehicle.year + '</div></div>' +
|
||||||
|
'<div><div class="vh-label">Motor</div><div class="vh-value">' + esc(currentVehicle.engine) + '</div></div>' +
|
||||||
|
(currentVehicle.trim_level ? '<div><div class="vh-label">Trim</div><div class="vh-value">' + esc(currentVehicle.trim_level) + '</div></div>' : '') +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="vh-actions">' +
|
||||||
|
'<button class="btn btn-secondary" id="btn-back-vehicles">◀ Volver</button>' +
|
||||||
|
'<button class="btn btn-primary" id="btn-complete-vehicle">Terminado ✓</button>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
document.getElementById('btn-back-vehicles').addEventListener('click', backToVehicles);
|
||||||
|
document.getElementById('btn-complete-vehicle').addEventListener('click', completeVehicle);
|
||||||
|
|
||||||
|
// Build groups by category
|
||||||
|
renderGroups(res.groups, vehicleParts);
|
||||||
|
updateProgress();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function backToVehicles() {
|
||||||
|
document.getElementById('oem-vehicle-select').style.display = 'block';
|
||||||
|
document.getElementById('oem-part-entry').style.display = 'none';
|
||||||
|
currentMye = null;
|
||||||
|
loadVehicles();
|
||||||
|
}
|
||||||
|
|
||||||
|
function completeVehicle() {
|
||||||
|
if (vehicleParts.length === 0) {
|
||||||
|
toast('Registra al menos una parte antes de marcar como terminado', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
api('/api/captura/vehicles/' + currentMye + '/status', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ status: 'completed' })
|
||||||
|
}).then(function () {
|
||||||
|
toast('Vehiculo completado');
|
||||||
|
backToVehicles();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Render groups/categories ---
|
||||||
|
function renderGroups(groups, parts) {
|
||||||
|
var container = document.getElementById('oem-groups-container');
|
||||||
|
// Group by category
|
||||||
|
var categories = {};
|
||||||
|
groups.forEach(function (g) {
|
||||||
|
if (!categories[g.category]) {
|
||||||
|
categories[g.category] = { id: g.id_part_category, groups: [] };
|
||||||
|
}
|
||||||
|
categories[g.category].groups.push(g);
|
||||||
|
});
|
||||||
|
|
||||||
|
var html = '';
|
||||||
|
Object.keys(categories).forEach(function (catName) {
|
||||||
|
var cat = categories[catName];
|
||||||
|
var catParts = parts.filter(function (p) {
|
||||||
|
return cat.groups.some(function (g) { return g.id_part_group === p.group_id; });
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '<div class="category-section">' +
|
||||||
|
'<div class="category-header" data-cat="' + cat.id + '">' +
|
||||||
|
'<h3>' + esc(catName) + ' (' + catParts.length + ')</h3>' +
|
||||||
|
'<span class="cat-toggle">▼</span></div>' +
|
||||||
|
'<div class="category-body" data-cat-body="' + cat.id + '">';
|
||||||
|
|
||||||
|
cat.groups.forEach(function (g) {
|
||||||
|
var groupParts = parts.filter(function (p) { return p.group_id === g.id_part_group; });
|
||||||
|
html += '<div class="group-section" data-group="' + g.id_part_group + '">' +
|
||||||
|
'<div class="group-name">' + esc(g.group_name) + '</div>' +
|
||||||
|
'<div class="part-rows" data-group-parts="' + g.id_part_group + '">';
|
||||||
|
|
||||||
|
groupParts.forEach(function (p) {
|
||||||
|
html += savedPartRow(p);
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</div>' +
|
||||||
|
'<button class="btn-add-part" data-group-id="' + g.id_part_group + '">+ Agregar pieza</button>' +
|
||||||
|
'</div>';
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</div></div>';
|
||||||
|
});
|
||||||
|
|
||||||
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
// Category toggle
|
||||||
|
container.querySelectorAll('.category-header').forEach(function (ch) {
|
||||||
|
ch.addEventListener('click', function () {
|
||||||
|
var catId = ch.getAttribute('data-cat');
|
||||||
|
var body = container.querySelector('[data-cat-body="' + catId + '"]');
|
||||||
|
ch.classList.toggle('collapsed');
|
||||||
|
body.classList.toggle('collapsed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add part buttons
|
||||||
|
container.querySelectorAll('.btn-add-part').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
addPartRow(parseInt(btn.getAttribute('data-group-id')), btn);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function savedPartRow(p) {
|
||||||
|
return '<div class="part-row saved" data-fitment-id="' + p.id_vehicle_part + '">' +
|
||||||
|
'<input class="pr-oem" value="' + esc(p.oem_part_number) + '" readonly>' +
|
||||||
|
'<input class="pr-name" value="' + esc(p.name_part || '') + '" readonly>' +
|
||||||
|
'<input class="pr-qty" value="' + (p.quantity_required || 1) + '" readonly>' +
|
||||||
|
'<button class="pr-btn pr-delete" title="Eliminar">✕</button>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPartRow(groupId, addBtn) {
|
||||||
|
var rowsContainer = document.querySelector('[data-group-parts="' + groupId + '"]');
|
||||||
|
var row = document.createElement('div');
|
||||||
|
row.className = 'part-row';
|
||||||
|
row.innerHTML = '<input class="pr-oem" placeholder="# OEM" data-group="' + groupId + '">' +
|
||||||
|
'<input class="pr-name" placeholder="Nombre pieza">' +
|
||||||
|
'<input class="pr-qty" value="1" type="number" min="1">' +
|
||||||
|
'<button class="pr-btn pr-save" title="Guardar">✓</button>' +
|
||||||
|
'<button class="pr-btn pr-delete" title="Quitar">✕</button>';
|
||||||
|
|
||||||
|
rowsContainer.appendChild(row);
|
||||||
|
|
||||||
|
// Focus OEM field
|
||||||
|
row.querySelector('.pr-oem').focus();
|
||||||
|
|
||||||
|
// OEM blur: check if exists
|
||||||
|
row.querySelector('.pr-oem').addEventListener('blur', function () {
|
||||||
|
var oem = this.value.trim();
|
||||||
|
if (!oem) return;
|
||||||
|
api('/api/captura/parts/check-oem?oem=' + encodeURIComponent(oem)).then(function (res) {
|
||||||
|
if (res.exists) {
|
||||||
|
row.querySelector('.pr-name').value = res.part.name_part || '';
|
||||||
|
row.querySelector('.pr-name').style.borderColor = 'var(--success)';
|
||||||
|
row.dataset.existingPartId = res.part.id_part;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save
|
||||||
|
row.querySelector('.pr-save').addEventListener('click', function () {
|
||||||
|
savePart(row, groupId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete (unsaved)
|
||||||
|
row.querySelector('.pr-delete').addEventListener('click', function () {
|
||||||
|
row.remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function savePart(row, groupId) {
|
||||||
|
var oem = row.querySelector('.pr-oem').value.trim();
|
||||||
|
var name = row.querySelector('.pr-name').value.trim();
|
||||||
|
var qty = parseInt(row.querySelector('.pr-qty').value) || 1;
|
||||||
|
|
||||||
|
if (!oem) {
|
||||||
|
toast('Ingresa el numero OEM', 'error');
|
||||||
|
row.querySelector('.pr-oem').focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var saveBtn = row.querySelector('.pr-save');
|
||||||
|
saveBtn.disabled = true;
|
||||||
|
saveBtn.textContent = '...';
|
||||||
|
|
||||||
|
// Check if part already exists
|
||||||
|
var existingId = row.dataset.existingPartId;
|
||||||
|
|
||||||
|
function createFitment(partId) {
|
||||||
|
api('/api/admin/fitment', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model_year_engine_id: currentMye,
|
||||||
|
part_id: partId,
|
||||||
|
quantity_required: qty
|
||||||
|
})
|
||||||
|
}).then(function (res) {
|
||||||
|
// Replace row with saved version
|
||||||
|
var newPart = {
|
||||||
|
id_vehicle_part: res.id,
|
||||||
|
part_id: partId,
|
||||||
|
oem_part_number: oem,
|
||||||
|
name_part: name,
|
||||||
|
quantity_required: qty,
|
||||||
|
group_id: groupId
|
||||||
|
};
|
||||||
|
vehicleParts.push(newPart);
|
||||||
|
row.outerHTML = savedPartRow(newPart);
|
||||||
|
updateProgress();
|
||||||
|
toast('Parte guardada: ' + oem);
|
||||||
|
|
||||||
|
// Re-attach delete handlers
|
||||||
|
attachDeleteHandlers();
|
||||||
|
}).catch(function (err) {
|
||||||
|
toast(err.message, 'error');
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
saveBtn.textContent = '\u2713';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingId) {
|
||||||
|
createFitment(parseInt(existingId));
|
||||||
|
} else {
|
||||||
|
// Create part first
|
||||||
|
api('/api/admin/parts', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
oem_part_number: oem,
|
||||||
|
name: name || oem,
|
||||||
|
group_id: groupId
|
||||||
|
})
|
||||||
|
}).then(function (res) {
|
||||||
|
createFitment(res.id);
|
||||||
|
}).catch(function (err) {
|
||||||
|
toast(err.message, 'error');
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
saveBtn.textContent = '\u2713';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachDeleteHandlers() {
|
||||||
|
document.querySelectorAll('.part-row.saved .pr-delete').forEach(function (btn) {
|
||||||
|
btn.onclick = function () {
|
||||||
|
var row = btn.closest('.part-row');
|
||||||
|
var fitmentId = row.getAttribute('data-fitment-id');
|
||||||
|
if (!fitmentId) { row.remove(); return; }
|
||||||
|
|
||||||
|
api('/api/admin/fitment/' + fitmentId, { method: 'DELETE' }).then(function () {
|
||||||
|
vehicleParts = vehicleParts.filter(function (p) {
|
||||||
|
return p.id_vehicle_part !== parseInt(fitmentId);
|
||||||
|
});
|
||||||
|
row.remove();
|
||||||
|
updateProgress();
|
||||||
|
toast('Parte eliminada');
|
||||||
|
}).catch(function (err) {
|
||||||
|
toast(err.message, 'error');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgress() {
|
||||||
|
var count = vehicleParts.length;
|
||||||
|
var totalGroups = 63;
|
||||||
|
var pct = Math.min(100, Math.round((count / totalGroups) * 100));
|
||||||
|
document.getElementById('oem-progress-fill').style.width = pct + '%';
|
||||||
|
document.getElementById('oem-progress-text').textContent = count + ' partes registradas';
|
||||||
|
|
||||||
|
// Update category counts
|
||||||
|
document.querySelectorAll('.category-header h3').forEach(function (h3) {
|
||||||
|
var catSection = h3.closest('.category-section');
|
||||||
|
var rows = catSection.querySelectorAll('.part-row.saved');
|
||||||
|
var catName = h3.textContent.replace(/\s*\(\d+\)$/, '');
|
||||||
|
h3.textContent = catName + ' (' + rows.length + ')';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// SECTION 2: Aftermarket / Interchange
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
var aftermarketPage = 1;
|
||||||
|
|
||||||
|
function loadPartsWithoutAftermarket(page) {
|
||||||
|
page = page || 1;
|
||||||
|
aftermarketPage = page;
|
||||||
|
var search = document.getElementById('aftermarket-search').value;
|
||||||
|
var list = document.getElementById('aftermarket-list');
|
||||||
|
list.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||||||
|
|
||||||
|
var params = '?page=' + page + '&per_page=20';
|
||||||
|
if (search) params += '&search=' + encodeURIComponent(search);
|
||||||
|
|
||||||
|
api('/api/captura/parts/without-aftermarket' + params).then(function (res) {
|
||||||
|
var data = res.data || [];
|
||||||
|
if (data.length === 0) {
|
||||||
|
list.innerHTML = '<div class="empty-state"><div class="es-icon">✅</div><div class="es-text">No hay piezas sin intercambios</div></div>';
|
||||||
|
document.getElementById('aftermarket-pagination').innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.innerHTML = data.map(function (p) {
|
||||||
|
return '<div class="part-detail-card" data-part-id="' + p.id_part + '">' +
|
||||||
|
'<div class="pdc-header">' +
|
||||||
|
'<div><span class="pdc-oem">' + esc(p.oem_part_number) + '</span>' +
|
||||||
|
' <span class="pdc-name">' + esc(p.name_part) + '</span></div>' +
|
||||||
|
'<span class="pdc-group">' + esc(p.category) + ' › ' + esc(p.group_name) + '</span></div>' +
|
||||||
|
'<div class="aftermarket-existing" data-af-list="' + p.id_part + '"></div>' +
|
||||||
|
'<div class="aftermarket-form" data-af-form="' + p.id_part + '">' +
|
||||||
|
'<div class="af-field"><label>Fabricante</label>' +
|
||||||
|
'<select class="af-manufacturer">' + manufacturerOptions() + '</select></div>' +
|
||||||
|
'<div class="af-field"><label># Aftermarket</label>' +
|
||||||
|
'<input class="af-partnum" placeholder="Ej: MK1234"></div>' +
|
||||||
|
'<div class="af-field"><label>Nombre</label>' +
|
||||||
|
'<input class="af-name" placeholder="Nombre pieza"></div>' +
|
||||||
|
'<div class="af-field"><label>Calidad</label>' +
|
||||||
|
'<select class="af-quality">' +
|
||||||
|
'<option value="standard">Standard</option>' +
|
||||||
|
'<option value="economy">Economy</option>' +
|
||||||
|
'<option value="oem">OEM</option>' +
|
||||||
|
'<option value="premium">Premium</option></select></div>' +
|
||||||
|
'<div class="af-field"><label>Precio USD</label>' +
|
||||||
|
'<input class="af-price" type="number" step="0.01" placeholder="0.00" style="width:80px"></div>' +
|
||||||
|
'<div class="af-field"><label>Garantia (meses)</label>' +
|
||||||
|
'<input class="af-warranty" type="number" placeholder="12" style="width:70px"></div>' +
|
||||||
|
'<button class="btn btn-primary af-save-btn" style="padding:0.4rem 1rem">+ Agregar</button>' +
|
||||||
|
'</div></div>';
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// Load existing aftermarket for each part
|
||||||
|
data.forEach(function (p) {
|
||||||
|
loadPartAftermarket(p.id_part);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save handlers
|
||||||
|
list.querySelectorAll('.af-save-btn').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
var card = btn.closest('.part-detail-card');
|
||||||
|
saveAftermarket(card);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
renderPagination('aftermarket-pagination', res.pagination, function (p) {
|
||||||
|
loadPartsWithoutAftermarket(p);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function manufacturerOptions() {
|
||||||
|
return manufacturers.map(function (m) {
|
||||||
|
return '<option value="' + m.id + '">' + esc(m.name) + '</option>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPartAftermarket(partId) {
|
||||||
|
api('/api/captura/parts/' + partId + '/aftermarket').then(function (items) {
|
||||||
|
var container = document.querySelector('[data-af-list="' + partId + '"]');
|
||||||
|
if (items.length === 0) {
|
||||||
|
container.innerHTML = '<p style="font-size:0.8rem;color:var(--text-secondary);margin-bottom:0.5rem">Sin intercambios registrados</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var html = '<table class="aftermarket-table"><thead><tr>' +
|
||||||
|
'<th>Fabricante</th><th># Parte</th><th>Nombre</th><th>Calidad</th><th>Precio</th><th>Garantia</th></tr></thead><tbody>';
|
||||||
|
items.forEach(function (a) {
|
||||||
|
html += '<tr><td>' + esc(a.manufacturer) + '</td><td>' + esc(a.part_number) +
|
||||||
|
'</td><td>' + esc(a.name || '') + '</td><td>' + esc(a.quality || '') +
|
||||||
|
'</td><td>' + (a.price_usd ? '$' + a.price_usd : '') +
|
||||||
|
'</td><td>' + (a.warranty_months || '') + '</td></tr>';
|
||||||
|
});
|
||||||
|
html += '</tbody></table>';
|
||||||
|
container.innerHTML = html;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAftermarket(card) {
|
||||||
|
var partId = card.getAttribute('data-part-id');
|
||||||
|
var manufacturer = card.querySelector('.af-manufacturer').value;
|
||||||
|
var partNumber = card.querySelector('.af-partnum').value.trim();
|
||||||
|
var name = card.querySelector('.af-name').value.trim();
|
||||||
|
var quality = card.querySelector('.af-quality').value;
|
||||||
|
var price = card.querySelector('.af-price').value;
|
||||||
|
var warranty = card.querySelector('.af-warranty').value;
|
||||||
|
|
||||||
|
if (!partNumber) {
|
||||||
|
toast('Ingresa el numero de parte aftermarket', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
api('/api/admin/aftermarket', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
oem_part_id: parseInt(partId),
|
||||||
|
manufacturer_id: parseInt(manufacturer),
|
||||||
|
part_number: partNumber,
|
||||||
|
name: name,
|
||||||
|
quality_tier: quality,
|
||||||
|
price_usd: price ? parseFloat(price) : null,
|
||||||
|
warranty_months: warranty ? parseInt(warranty) : null
|
||||||
|
})
|
||||||
|
}).then(function () {
|
||||||
|
toast('Intercambio guardado: ' + partNumber);
|
||||||
|
// Clear form
|
||||||
|
card.querySelector('.af-partnum').value = '';
|
||||||
|
card.querySelector('.af-name').value = '';
|
||||||
|
card.querySelector('.af-price').value = '';
|
||||||
|
card.querySelector('.af-warranty').value = '';
|
||||||
|
// Reload aftermarket list
|
||||||
|
loadPartAftermarket(parseInt(partId));
|
||||||
|
}).catch(function (err) {
|
||||||
|
toast(err.message, 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// SECTION 3: Images
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
var imagePage = 1;
|
||||||
|
|
||||||
|
function loadPartsWithoutImage(page) {
|
||||||
|
page = page || 1;
|
||||||
|
imagePage = page;
|
||||||
|
var search = document.getElementById('image-search').value;
|
||||||
|
var list = document.getElementById('image-list');
|
||||||
|
list.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||||||
|
|
||||||
|
var params = '?page=' + page + '&per_page=20';
|
||||||
|
if (search) params += '&search=' + encodeURIComponent(search);
|
||||||
|
|
||||||
|
api('/api/captura/parts/without-image' + params).then(function (res) {
|
||||||
|
var data = res.data || [];
|
||||||
|
if (data.length === 0) {
|
||||||
|
list.innerHTML = '<div class="empty-state"><div class="es-icon">📷</div><div class="es-text">No hay piezas sin imagen</div></div>';
|
||||||
|
document.getElementById('image-pagination').innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.innerHTML = data.map(function (p) {
|
||||||
|
return '<div class="image-card" data-part-id="' + p.id_part + '">' +
|
||||||
|
'<div class="ic-preview"><span>Sin imagen</span></div>' +
|
||||||
|
'<div class="ic-info">' +
|
||||||
|
'<div class="ic-oem">' + esc(p.oem_part_number) + '</div>' +
|
||||||
|
'<div class="ic-name">' + esc(p.name_part) + ' · ' + esc(p.group_name) + '</div>' +
|
||||||
|
'<div class="ic-upload">' +
|
||||||
|
'<input type="file" accept="image/jpeg,image/png,image/webp" class="ic-file-input">' +
|
||||||
|
'<button class="btn btn-primary ic-upload-btn" style="padding:0.3rem 0.8rem;font-size:0.8rem" disabled>Subir</button>' +
|
||||||
|
'</div></div></div>';
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// File input change → enable upload button and show preview
|
||||||
|
list.querySelectorAll('.ic-file-input').forEach(function (input) {
|
||||||
|
input.addEventListener('change', function () {
|
||||||
|
var card = input.closest('.image-card');
|
||||||
|
var btn = card.querySelector('.ic-upload-btn');
|
||||||
|
var preview = card.querySelector('.ic-preview');
|
||||||
|
|
||||||
|
if (input.files && input.files[0]) {
|
||||||
|
btn.disabled = false;
|
||||||
|
|
||||||
|
// Show preview
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function (e) {
|
||||||
|
preview.innerHTML = '<img src="' + e.target.result + '">';
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(input.files[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Upload button
|
||||||
|
list.querySelectorAll('.ic-upload-btn').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
var card = btn.closest('.image-card');
|
||||||
|
uploadImage(card);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
renderPagination('image-pagination', res.pagination, function (p) {
|
||||||
|
loadPartsWithoutImage(p);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadImage(card) {
|
||||||
|
var partId = card.getAttribute('data-part-id');
|
||||||
|
var fileInput = card.querySelector('.ic-file-input');
|
||||||
|
var btn = card.querySelector('.ic-upload-btn');
|
||||||
|
|
||||||
|
if (!fileInput.files || !fileInput.files[0]) return;
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Subiendo...';
|
||||||
|
|
||||||
|
var formData = new FormData();
|
||||||
|
formData.append('image', fileInput.files[0]);
|
||||||
|
|
||||||
|
fetch(API + '/api/captura/parts/' + partId + '/image', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
}).then(function (r) { return r.json(); })
|
||||||
|
.then(function (res) {
|
||||||
|
if (res.error) throw new Error(res.error);
|
||||||
|
toast('Imagen subida correctamente');
|
||||||
|
// Remove card from list
|
||||||
|
card.style.opacity = '0.3';
|
||||||
|
setTimeout(function () { card.remove(); }, 500);
|
||||||
|
}).catch(function (err) {
|
||||||
|
toast(err.message, 'error');
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Subir';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Init
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
loadBrands();
|
||||||
|
loadVehicles();
|
||||||
|
|
||||||
|
// Pre-load manufacturers for Section 2
|
||||||
|
api('/api/captura/manufacturers').then(function (data) {
|
||||||
|
manufacturers = data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make functions globally accessible for inline onclick handlers
|
||||||
|
window.loadPartsWithoutAftermarket = loadPartsWithoutAftermarket;
|
||||||
|
window.loadPartsWithoutImage = loadPartsWithoutImage;
|
||||||
|
|
||||||
|
init();
|
||||||
|
})();
|
||||||
282
dashboard/cuentas.css
Normal file
282
dashboard/cuentas.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
102
dashboard/cuentas.html
Normal file
102
dashboard/cuentas.html
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Cuentas por Cobrar — NEXUS AUTOPARTS</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Orbitron:wght@700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="/shared.css">
|
||||||
|
<link rel="stylesheet" href="/cuentas.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="shared-nav"></div>
|
||||||
|
|
||||||
|
<div class="cuentas-container">
|
||||||
|
<!-- Customer List View -->
|
||||||
|
<div id="list-view">
|
||||||
|
<div class="cuentas-search">
|
||||||
|
<input id="customer-search" type="text" placeholder="Buscar cliente por nombre o RFC...">
|
||||||
|
</div>
|
||||||
|
<div id="customer-grid" class="customer-grid"></div>
|
||||||
|
<div id="customer-pagination" class="cuentas-pagination"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Customer Detail View -->
|
||||||
|
<div id="detail-view" class="detail-view">
|
||||||
|
<div class="detail-header">
|
||||||
|
<div class="dh-info">
|
||||||
|
<div class="dh-field"><div class="dh-label">Cliente</div><div class="dh-value accent" id="dh-name"></div></div>
|
||||||
|
<div class="dh-field"><div class="dh-label">RFC</div><div class="dh-value" id="dh-rfc"></div></div>
|
||||||
|
<div class="dh-field"><div class="dh-label">Saldo</div><div class="dh-value" id="dh-balance"></div></div>
|
||||||
|
<div class="dh-field"><div class="dh-label">Limite</div><div class="dh-value" id="dh-limit"></div></div>
|
||||||
|
<div class="dh-field"><div class="dh-label">Plazo</div><div class="dh-value" id="dh-terms"></div></div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary" id="btn-back-list">« Volver</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-columns">
|
||||||
|
<!-- Invoices -->
|
||||||
|
<div class="detail-card">
|
||||||
|
<h3>Facturas</h3>
|
||||||
|
<table class="detail-table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Folio</th><th>Fecha</th><th>Total</th><th>Pagado</th><th>Estado</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="invoice-list"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Payments + Form -->
|
||||||
|
<div class="detail-card">
|
||||||
|
<h3>Pagos</h3>
|
||||||
|
<table class="detail-table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Fecha</th><th>Monto</th><th>Metodo</th><th>Ref</th><th>Factura</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="payment-list"></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="payment-form">
|
||||||
|
<h4>Registrar Pago</h4>
|
||||||
|
<div class="pf-row">
|
||||||
|
<div class="pf-field">
|
||||||
|
<label>Monto *</label>
|
||||||
|
<input id="pay-amount" type="number" step="0.01" min="0.01" placeholder="0.00" style="width:120px">
|
||||||
|
</div>
|
||||||
|
<div class="pf-field">
|
||||||
|
<label>Metodo</label>
|
||||||
|
<select id="pay-method">
|
||||||
|
<option value="efectivo">Efectivo</option>
|
||||||
|
<option value="transferencia">Transferencia</option>
|
||||||
|
<option value="cheque">Cheque</option>
|
||||||
|
<option value="tarjeta">Tarjeta</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="pf-field">
|
||||||
|
<label>Referencia</label>
|
||||||
|
<input id="pay-reference" placeholder="# ref" style="width:120px">
|
||||||
|
</div>
|
||||||
|
<div class="pf-field">
|
||||||
|
<label>Aplicar a factura</label>
|
||||||
|
<select id="pay-invoice">
|
||||||
|
<option value="">Abono general</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pf-row">
|
||||||
|
<div class="pf-field" style="flex:1">
|
||||||
|
<label>Notas</label>
|
||||||
|
<input id="pay-notes" placeholder="Notas del pago" style="width:100%">
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" id="btn-pay" style="align-self:flex-end;padding:0.4rem 1.2rem">Registrar Pago</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/nav.js"></script>
|
||||||
|
<script src="/cuentas.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
222
dashboard/cuentas.js
Normal file
222
dashboard/cuentas.js
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
/**
|
||||||
|
* cuentas.js — Accounts receivable logic for Nexus Autoparts
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var API = '';
|
||||||
|
var currentCustomerId = null;
|
||||||
|
var customerPage = 1;
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Utility
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
function toast(msg, type) {
|
||||||
|
var el = document.createElement('div');
|
||||||
|
el.className = 'toast ' + (type || 'success');
|
||||||
|
el.textContent = msg;
|
||||||
|
document.body.appendChild(el);
|
||||||
|
setTimeout(function () { el.remove(); }, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function api(path, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
return fetch(API + path, opts).then(function (r) {
|
||||||
|
if (!r.ok) return r.json().then(function (d) { throw new Error(d.error || 'Error'); });
|
||||||
|
return r.json();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(s) {
|
||||||
|
if (!s) return '';
|
||||||
|
var d = document.createElement('div');
|
||||||
|
d.textContent = s;
|
||||||
|
return d.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmt(n) {
|
||||||
|
return '$' + (parseFloat(n) || 0).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtDate(d) {
|
||||||
|
if (!d) return '';
|
||||||
|
var dt = new Date(d);
|
||||||
|
return dt.toLocaleDateString('es-MX', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Customer List
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
var searchTimer = null;
|
||||||
|
document.getElementById('customer-search').addEventListener('input', function () {
|
||||||
|
clearTimeout(searchTimer);
|
||||||
|
searchTimer = setTimeout(function () {
|
||||||
|
customerPage = 1;
|
||||||
|
loadCustomers();
|
||||||
|
}, 400);
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadCustomers() {
|
||||||
|
var search = document.getElementById('customer-search').value;
|
||||||
|
var grid = document.getElementById('customer-grid');
|
||||||
|
grid.innerHTML = '<div class="empty-state">Cargando...</div>';
|
||||||
|
|
||||||
|
var params = '?page=' + customerPage + '&per_page=30';
|
||||||
|
if (search) params += '&search=' + encodeURIComponent(search);
|
||||||
|
|
||||||
|
api('/api/pos/customers' + params).then(function (res) {
|
||||||
|
var data = res.data || [];
|
||||||
|
if (data.length === 0) {
|
||||||
|
grid.innerHTML = '<div class="empty-state">No se encontraron clientes</div>';
|
||||||
|
document.getElementById('customer-pagination').innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
grid.innerHTML = data.map(function (c) {
|
||||||
|
return '<div class="customer-card-item" data-id="' + c.id_customer + '">' +
|
||||||
|
'<div class="cci-name">' + esc(c.name) + '</div>' +
|
||||||
|
'<div class="cci-rfc">' + esc(c.rfc || 'Sin RFC') + '</div>' +
|
||||||
|
'<div class="cci-balance-row">' +
|
||||||
|
'<span class="cci-balance ' + (c.balance > 0 ? 'positive' : 'zero') + '">' + fmt(c.balance) + '</span>' +
|
||||||
|
'<span class="cci-limit">Limite: ' + fmt(c.credit_limit) + '</span></div></div>';
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
grid.querySelectorAll('.customer-card-item').forEach(function (card) {
|
||||||
|
card.addEventListener('click', function () {
|
||||||
|
showCustomerDetail(parseInt(card.getAttribute('data-id')));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
var pag = res.pagination;
|
||||||
|
var pagEl = document.getElementById('customer-pagination');
|
||||||
|
if (pag.total_pages <= 1) { pagEl.innerHTML = ''; return; }
|
||||||
|
pagEl.innerHTML = '<button ' + (pag.page <= 1 ? 'disabled' : '') + ' data-p="' + (pag.page - 1) + '">«</button>' +
|
||||||
|
'<span class="page-info">Pag ' + pag.page + '/' + pag.total_pages + '</span>' +
|
||||||
|
'<button ' + (pag.page >= pag.total_pages ? 'disabled' : '') + ' data-p="' + (pag.page + 1) + '">»</button>';
|
||||||
|
pagEl.querySelectorAll('button').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
customerPage = parseInt(btn.getAttribute('data-p'));
|
||||||
|
loadCustomers();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).catch(function (err) {
|
||||||
|
console.error('Error loading customers:', err);
|
||||||
|
grid.innerHTML = '<div class="empty-state">Error al cargar clientes</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Customer Detail
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
function showCustomerDetail(customerId) {
|
||||||
|
currentCustomerId = customerId;
|
||||||
|
document.getElementById('list-view').style.display = 'none';
|
||||||
|
document.getElementById('detail-view').style.display = 'block';
|
||||||
|
|
||||||
|
api('/api/pos/customers/' + customerId + '/statement').then(function (res) {
|
||||||
|
var c = res.customer;
|
||||||
|
document.getElementById('dh-name').textContent = c.name;
|
||||||
|
document.getElementById('dh-rfc').textContent = c.rfc || 'Sin RFC';
|
||||||
|
|
||||||
|
var balEl = document.getElementById('dh-balance');
|
||||||
|
balEl.textContent = fmt(c.balance);
|
||||||
|
balEl.className = 'dh-value ' + (c.balance > 0 ? 'danger' : 'success');
|
||||||
|
|
||||||
|
document.getElementById('dh-limit').textContent = fmt(c.credit_limit);
|
||||||
|
document.getElementById('dh-terms').textContent = c.payment_terms + ' dias';
|
||||||
|
|
||||||
|
// Invoices
|
||||||
|
var invBody = document.getElementById('invoice-list');
|
||||||
|
if (res.invoices.length === 0) {
|
||||||
|
invBody.innerHTML = '<tr><td colspan="5" class="empty-state">Sin facturas</td></tr>';
|
||||||
|
} else {
|
||||||
|
invBody.innerHTML = res.invoices.map(function (i) {
|
||||||
|
return '<tr>' +
|
||||||
|
'<td style="font-family:monospace;font-weight:600">' + esc(i.folio) + '</td>' +
|
||||||
|
'<td>' + fmtDate(i.date_issued) + '</td>' +
|
||||||
|
'<td>' + fmt(i.total) + '</td>' +
|
||||||
|
'<td>' + fmt(i.amount_paid) + '</td>' +
|
||||||
|
'<td><span class="status-badge ' + i.status + '">' + i.status + '</span></td></tr>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payments
|
||||||
|
var payBody = document.getElementById('payment-list');
|
||||||
|
if (res.payments.length === 0) {
|
||||||
|
payBody.innerHTML = '<tr><td colspan="5" class="empty-state">Sin pagos</td></tr>';
|
||||||
|
} else {
|
||||||
|
payBody.innerHTML = res.payments.map(function (p) {
|
||||||
|
return '<tr>' +
|
||||||
|
'<td>' + fmtDate(p.date_payment) + '</td>' +
|
||||||
|
'<td style="font-weight:600;color:var(--success)">' + fmt(p.amount) + '</td>' +
|
||||||
|
'<td>' + esc(p.payment_method) + '</td>' +
|
||||||
|
'<td>' + esc(p.reference || '') + '</td>' +
|
||||||
|
'<td>' + esc(p.invoice_folio || 'General') + '</td></tr>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate invoice dropdown for payment form
|
||||||
|
var invSelect = document.getElementById('pay-invoice');
|
||||||
|
invSelect.innerHTML = '<option value="">Abono general</option>';
|
||||||
|
res.invoices.filter(function (i) { return i.status !== 'paid' && i.status !== 'cancelled'; })
|
||||||
|
.forEach(function (i) {
|
||||||
|
invSelect.innerHTML += '<option value="' + i.id_invoice + '">' +
|
||||||
|
i.folio + ' — ' + fmt(i.total - i.amount_paid) + ' pendiente</option>';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btn-back-list').addEventListener('click', function () {
|
||||||
|
document.getElementById('detail-view').style.display = 'none';
|
||||||
|
document.getElementById('list-view').style.display = 'block';
|
||||||
|
currentCustomerId = null;
|
||||||
|
loadCustomers();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Register Payment
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
document.getElementById('btn-pay').addEventListener('click', function () {
|
||||||
|
var amount = parseFloat(document.getElementById('pay-amount').value);
|
||||||
|
if (!amount || amount <= 0) {
|
||||||
|
toast('Ingresa un monto valido', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var invoiceId = document.getElementById('pay-invoice').value;
|
||||||
|
|
||||||
|
api('/api/pos/payments', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
customer_id: currentCustomerId,
|
||||||
|
amount: amount,
|
||||||
|
payment_method: document.getElementById('pay-method').value,
|
||||||
|
reference: document.getElementById('pay-reference').value.trim() || null,
|
||||||
|
invoice_id: invoiceId ? parseInt(invoiceId) : null,
|
||||||
|
notes: document.getElementById('pay-notes').value.trim() || null
|
||||||
|
})
|
||||||
|
}).then(function () {
|
||||||
|
toast('Pago de ' + fmt(amount) + ' registrado');
|
||||||
|
// Clear form
|
||||||
|
document.getElementById('pay-amount').value = '';
|
||||||
|
document.getElementById('pay-reference').value = '';
|
||||||
|
document.getElementById('pay-notes').value = '';
|
||||||
|
// Refresh detail
|
||||||
|
showCustomerDetail(currentCustomerId);
|
||||||
|
}).catch(function (err) {
|
||||||
|
toast(err.message, 'error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Init
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
loadCustomers();
|
||||||
|
})();
|
||||||
418
dashboard/pos.css
Normal file
418
dashboard/pos.css
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
113
dashboard/pos.html
Normal file
113
dashboard/pos.html
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Punto de Venta — NEXUS AUTOPARTS</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Orbitron:wght@700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="/shared.css">
|
||||||
|
<link rel="stylesheet" href="/pos.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="shared-nav"></div>
|
||||||
|
|
||||||
|
<div class="pos-container">
|
||||||
|
<div class="pos-layout">
|
||||||
|
<!-- LEFT: Main area -->
|
||||||
|
<div class="pos-main">
|
||||||
|
<!-- Customer selection -->
|
||||||
|
<div class="customer-bar" style="position:relative">
|
||||||
|
<div id="customer-select" style="flex:1;position:relative">
|
||||||
|
<input class="cb-search" id="customer-search" type="text" placeholder="Buscar cliente por nombre o RFC...">
|
||||||
|
<div id="customer-dropdown" class="customer-dropdown" style="display:none"></div>
|
||||||
|
</div>
|
||||||
|
<div id="customer-info" class="cb-selected" style="display:none">
|
||||||
|
<span class="cb-name" id="sel-customer-name"></span>
|
||||||
|
<span class="cb-rfc" id="sel-customer-rfc"></span>
|
||||||
|
<span class="cb-balance" id="sel-customer-balance"></span>
|
||||||
|
<button class="btn btn-secondary" style="padding:0.3rem 0.6rem;font-size:0.8rem" id="btn-change-customer">Cambiar</button>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary" id="btn-new-customer" style="padding:0.5rem 0.8rem;font-size:0.85rem">+ Nuevo</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Part search -->
|
||||||
|
<div class="part-search-wrap">
|
||||||
|
<input class="part-search" id="part-search" type="text" placeholder="Buscar parte por # OEM, # aftermarket o nombre...">
|
||||||
|
<div id="part-results" class="part-results"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cart -->
|
||||||
|
<div class="cart-card">
|
||||||
|
<h3>Carrito</h3>
|
||||||
|
<table class="cart-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Descripcion</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Cant</th>
|
||||||
|
<th>Costo</th>
|
||||||
|
<th>Margen%</th>
|
||||||
|
<th>Precio</th>
|
||||||
|
<th>Total</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="cart-body">
|
||||||
|
<tr><td colspan="8" class="cart-empty">Busca y agrega partes al carrito</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RIGHT: Summary -->
|
||||||
|
<div class="pos-sidebar">
|
||||||
|
<div class="invoice-summary">
|
||||||
|
<h3>Resumen de Factura</h3>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="sr-label">Articulos</span>
|
||||||
|
<span class="sr-value" id="sum-items">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="sr-label">Subtotal</span>
|
||||||
|
<span class="sr-value" id="sum-subtotal">$0.00</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row">
|
||||||
|
<span class="sr-label">IVA (16%)</span>
|
||||||
|
<span class="sr-value" id="sum-tax">$0.00</span>
|
||||||
|
</div>
|
||||||
|
<div class="summary-row total">
|
||||||
|
<span class="sr-label">Total</span>
|
||||||
|
<span class="sr-value" id="sum-total">$0.00</span>
|
||||||
|
</div>
|
||||||
|
<textarea class="invoice-notes" id="invoice-notes" placeholder="Notas de la factura (opcional)"></textarea>
|
||||||
|
<button class="btn-facturar" id="btn-facturar" disabled>Facturar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- New Customer Modal -->
|
||||||
|
<div id="modal-new-customer" class="modal-overlay" style="display:none">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3>Nuevo Cliente</h3>
|
||||||
|
<div class="modal-field"><label>Nombre *</label><input id="nc-name" required></div>
|
||||||
|
<div class="modal-field"><label>RFC</label><input id="nc-rfc" maxlength="13" placeholder="XAXX010101000"></div>
|
||||||
|
<div class="modal-field"><label>Razon Social</label><input id="nc-business"></div>
|
||||||
|
<div class="modal-field"><label>Telefono</label><input id="nc-phone"></div>
|
||||||
|
<div class="modal-field"><label>Email</label><input id="nc-email" type="email"></div>
|
||||||
|
<div class="modal-field"><label>Direccion</label><input id="nc-address"></div>
|
||||||
|
<div style="display:flex;gap:1rem">
|
||||||
|
<div class="modal-field" style="flex:1"><label>Limite de Credito</label><input id="nc-credit" type="number" value="0"></div>
|
||||||
|
<div class="modal-field" style="flex:1"><label>Dias de Credito</label><input id="nc-terms" type="number" value="30"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn btn-secondary" id="nc-cancel">Cancelar</button>
|
||||||
|
<button class="btn btn-primary" id="nc-save">Guardar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="/nav.js"></script>
|
||||||
|
<script src="/pos.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
413
dashboard/pos.js
Normal file
413
dashboard/pos.js
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
/**
|
||||||
|
* pos.js — Point of Sale logic for Nexus Autoparts
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var API = '';
|
||||||
|
var selectedCustomer = null;
|
||||||
|
var cart = [];
|
||||||
|
var defaultMargin = 30;
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Utility
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
function toast(msg, type) {
|
||||||
|
var el = document.createElement('div');
|
||||||
|
el.className = 'toast ' + (type || 'success');
|
||||||
|
el.textContent = msg;
|
||||||
|
document.body.appendChild(el);
|
||||||
|
setTimeout(function () { el.remove(); }, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function api(path, opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
return fetch(API + path, opts).then(function (r) {
|
||||||
|
if (!r.ok) return r.json().then(function (d) { throw new Error(d.error || 'Error'); });
|
||||||
|
return r.json();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(s) {
|
||||||
|
if (!s) return '';
|
||||||
|
var d = document.createElement('div');
|
||||||
|
d.textContent = s;
|
||||||
|
return d.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmt(n) {
|
||||||
|
return '$' + (parseFloat(n) || 0).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Customer Selection
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
var customerSearchTimer = null;
|
||||||
|
var customerSearchEl = document.getElementById('customer-search');
|
||||||
|
var customerDropdown = document.getElementById('customer-dropdown');
|
||||||
|
|
||||||
|
customerSearchEl.addEventListener('input', function () {
|
||||||
|
clearTimeout(customerSearchTimer);
|
||||||
|
var q = this.value.trim();
|
||||||
|
if (q.length < 2) { customerDropdown.style.display = 'none'; return; }
|
||||||
|
customerSearchTimer = setTimeout(function () {
|
||||||
|
api('/api/pos/customers?search=' + encodeURIComponent(q) + '&per_page=10')
|
||||||
|
.then(function (res) {
|
||||||
|
var data = res.data || [];
|
||||||
|
if (data.length === 0) {
|
||||||
|
customerDropdown.innerHTML = '<div style="padding:0.8rem;color:var(--text-secondary)">No se encontraron clientes</div>';
|
||||||
|
} else {
|
||||||
|
customerDropdown.innerHTML = data.map(function (c) {
|
||||||
|
return '<div class="customer-dropdown-item" data-id="' + c.id_customer + '">' +
|
||||||
|
'<div><span class="cdi-name">' + esc(c.name) + '</span>' +
|
||||||
|
(c.rfc ? ' <span class="cdi-rfc">' + esc(c.rfc) + '</span>' : '') + '</div>' +
|
||||||
|
'<span style="font-size:0.8rem;color:' + (c.balance > 0 ? 'var(--danger)' : 'var(--success)') + '">' +
|
||||||
|
fmt(c.balance) + '</span></div>';
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
customerDropdown.querySelectorAll('.customer-dropdown-item').forEach(function (item) {
|
||||||
|
item.addEventListener('click', function () {
|
||||||
|
selectCustomer(parseInt(item.getAttribute('data-id')));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
customerDropdown.style.display = 'block';
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
customerSearchEl.addEventListener('blur', function () {
|
||||||
|
setTimeout(function () { customerDropdown.style.display = 'none'; }, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
function selectCustomer(id) {
|
||||||
|
api('/api/pos/customers/' + id).then(function (c) {
|
||||||
|
selectedCustomer = c;
|
||||||
|
document.getElementById('customer-select').style.display = 'none';
|
||||||
|
var info = document.getElementById('customer-info');
|
||||||
|
info.style.display = 'flex';
|
||||||
|
document.getElementById('sel-customer-name').textContent = c.name;
|
||||||
|
document.getElementById('sel-customer-rfc').textContent = c.rfc || 'Sin RFC';
|
||||||
|
var balEl = document.getElementById('sel-customer-balance');
|
||||||
|
balEl.textContent = 'Saldo: ' + fmt(c.balance);
|
||||||
|
balEl.className = 'cb-balance ' + (c.balance > 0 ? 'positive' : 'zero');
|
||||||
|
customerDropdown.style.display = 'none';
|
||||||
|
updateFacturarBtn();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('btn-change-customer').addEventListener('click', function () {
|
||||||
|
selectedCustomer = null;
|
||||||
|
document.getElementById('customer-info').style.display = 'none';
|
||||||
|
document.getElementById('customer-select').style.display = 'block';
|
||||||
|
customerSearchEl.value = '';
|
||||||
|
customerSearchEl.focus();
|
||||||
|
updateFacturarBtn();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// New Customer Modal
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
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 name = document.getElementById('nc-name').value.trim();
|
||||||
|
if (!name) { toast('Ingresa el nombre del cliente', 'error'); return; }
|
||||||
|
|
||||||
|
api('/api/pos/customers', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: name,
|
||||||
|
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 (res) {
|
||||||
|
toast('Cliente creado: ' + name);
|
||||||
|
document.getElementById('modal-new-customer').style.display = 'none';
|
||||||
|
selectCustomer(res.id);
|
||||||
|
// Clear form
|
||||||
|
['nc-name','nc-rfc','nc-business','nc-phone','nc-email','nc-address'].forEach(function(id) {
|
||||||
|
document.getElementById(id).value = '';
|
||||||
|
});
|
||||||
|
document.getElementById('nc-credit').value = '0';
|
||||||
|
document.getElementById('nc-terms').value = '30';
|
||||||
|
}).catch(function (err) {
|
||||||
|
toast(err.message, 'error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Part Search — Autocomplete
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
var partSearchTimer = null;
|
||||||
|
var partSearchEl = document.getElementById('part-search');
|
||||||
|
var partResults = document.getElementById('part-results');
|
||||||
|
var searchResults = [];
|
||||||
|
var highlightIdx = -1;
|
||||||
|
|
||||||
|
function doPartSearch() {
|
||||||
|
var q = partSearchEl.value.trim();
|
||||||
|
if (q.length < 1) { partResults.style.display = 'none'; searchResults = []; return; }
|
||||||
|
clearTimeout(partSearchTimer);
|
||||||
|
partSearchTimer = setTimeout(function () {
|
||||||
|
api('/api/pos/search-parts?q=' + encodeURIComponent(q)).then(function (results) {
|
||||||
|
searchResults = results;
|
||||||
|
highlightIdx = -1;
|
||||||
|
renderSearchResults();
|
||||||
|
});
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSearchResults() {
|
||||||
|
if (searchResults.length === 0 && partSearchEl.value.trim().length > 0) {
|
||||||
|
partResults.innerHTML = '<div style="padding:0.8rem;color:var(--text-secondary)">No se encontraron partes para "' + esc(partSearchEl.value) + '"</div>';
|
||||||
|
partResults.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (searchResults.length === 0) { partResults.style.display = 'none'; return; }
|
||||||
|
|
||||||
|
partResults.innerHTML = searchResults.map(function (p, i) {
|
||||||
|
var active = i === highlightIdx ? ' part-result-active' : '';
|
||||||
|
return '<div class="part-result-item' + active + '" data-idx="' + i + '">' +
|
||||||
|
'<div><span class="pri-number">' + esc(p.oem_part_number) + '</span>' +
|
||||||
|
'<span class="pri-name">' + esc(p.name_part) + '</span></div>' +
|
||||||
|
'<div style="display:flex;align-items:center;gap:0.4rem">' +
|
||||||
|
'<span class="pri-type ' + p.part_type + '">' + p.part_type + '</span>' +
|
||||||
|
(p.cost_usd ? '<span style="font-size:0.8rem;color:var(--text-secondary)">' + fmt(p.cost_usd) + '</span>' : '') +
|
||||||
|
'<span style="font-size:0.75rem;color:var(--text-secondary)">' + esc(p.group_name || '') + '</span>' +
|
||||||
|
'</div></div>';
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
partResults.querySelectorAll('.part-result-item').forEach(function (item) {
|
||||||
|
item.addEventListener('mousedown', function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
selectSearchResult(parseInt(item.getAttribute('data-idx')));
|
||||||
|
});
|
||||||
|
item.addEventListener('mouseenter', function () {
|
||||||
|
highlightIdx = parseInt(item.getAttribute('data-idx'));
|
||||||
|
updateHighlight();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
partResults.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHighlight() {
|
||||||
|
partResults.querySelectorAll('.part-result-item').forEach(function (el, i) {
|
||||||
|
if (i === highlightIdx) {
|
||||||
|
el.classList.add('part-result-active');
|
||||||
|
el.scrollIntoView({ block: 'nearest' });
|
||||||
|
} else {
|
||||||
|
el.classList.remove('part-result-active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectSearchResult(idx) {
|
||||||
|
if (idx >= 0 && idx < searchResults.length) {
|
||||||
|
addToCart(searchResults[idx]);
|
||||||
|
partSearchEl.value = '';
|
||||||
|
partResults.style.display = 'none';
|
||||||
|
searchResults = [];
|
||||||
|
highlightIdx = -1;
|
||||||
|
partSearchEl.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
partSearchEl.addEventListener('input', doPartSearch);
|
||||||
|
|
||||||
|
partSearchEl.addEventListener('keydown', function (e) {
|
||||||
|
if (partResults.style.display === 'none' || searchResults.length === 0) return;
|
||||||
|
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
highlightIdx = Math.min(highlightIdx + 1, searchResults.length - 1);
|
||||||
|
updateHighlight();
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
highlightIdx = Math.max(highlightIdx - 1, 0);
|
||||||
|
updateHighlight();
|
||||||
|
} else if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (highlightIdx >= 0) {
|
||||||
|
selectSearchResult(highlightIdx);
|
||||||
|
} else if (searchResults.length === 1) {
|
||||||
|
selectSearchResult(0);
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
partResults.style.display = 'none';
|
||||||
|
highlightIdx = -1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
partSearchEl.addEventListener('focus', function () {
|
||||||
|
if (searchResults.length > 0) {
|
||||||
|
partResults.style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
partSearchEl.addEventListener('blur', function () {
|
||||||
|
setTimeout(function () { partResults.style.display = 'none'; }, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Cart
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
function addToCart(part) {
|
||||||
|
cart.push({
|
||||||
|
part_id: part.part_type === 'oem' ? part.id_part : null,
|
||||||
|
aftermarket_id: part.part_type === 'aftermarket' ? part.id_part : null,
|
||||||
|
description: (part.oem_part_number || '') + ' - ' + (part.name_part || ''),
|
||||||
|
part_type: part.part_type,
|
||||||
|
quantity: 1,
|
||||||
|
unit_cost: part.cost_usd || 0,
|
||||||
|
margin_pct: defaultMargin,
|
||||||
|
unit_price: (part.cost_usd || 0) * (1 + defaultMargin / 100)
|
||||||
|
});
|
||||||
|
renderCart();
|
||||||
|
partSearchEl.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCart() {
|
||||||
|
var tbody = document.getElementById('cart-body');
|
||||||
|
|
||||||
|
if (cart.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="8" class="cart-empty">Busca y agrega partes al carrito</td></tr>';
|
||||||
|
updateTotals();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = cart.map(function (item, i) {
|
||||||
|
var lineTotal = item.quantity * item.unit_price;
|
||||||
|
return '<tr>' +
|
||||||
|
'<td class="cart-desc">' + esc(item.description) + '</td>' +
|
||||||
|
'<td><span class="pri-type ' + item.part_type + '">' + item.part_type + '</span></td>' +
|
||||||
|
'<td><input class="cart-qty" type="number" min="1" value="' + item.quantity + '" data-idx="' + i + '" data-field="quantity"></td>' +
|
||||||
|
'<td><input class="cart-cost" type="number" step="0.01" value="' + item.unit_cost.toFixed(2) + '" data-idx="' + i + '" data-field="unit_cost"></td>' +
|
||||||
|
'<td><input class="cart-margin" type="number" step="1" value="' + item.margin_pct.toFixed(0) + '" data-idx="' + i + '" data-field="margin_pct">%</td>' +
|
||||||
|
'<td>' + fmt(item.unit_price) + '</td>' +
|
||||||
|
'<td>' + fmt(lineTotal) + '</td>' +
|
||||||
|
'<td><button class="cart-remove" data-idx="' + i + '">×</button></td>' +
|
||||||
|
'</tr>';
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// Input change handlers
|
||||||
|
tbody.querySelectorAll('input').forEach(function (input) {
|
||||||
|
input.addEventListener('change', function () {
|
||||||
|
var idx = parseInt(input.getAttribute('data-idx'));
|
||||||
|
var field = input.getAttribute('data-field');
|
||||||
|
var val = parseFloat(input.value) || 0;
|
||||||
|
cart[idx][field] = val;
|
||||||
|
|
||||||
|
// Recalculate price from cost + margin
|
||||||
|
if (field === 'unit_cost' || field === 'margin_pct') {
|
||||||
|
cart[idx].unit_price = cart[idx].unit_cost * (1 + cart[idx].margin_pct / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCart();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove handlers
|
||||||
|
tbody.querySelectorAll('.cart-remove').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
cart.splice(parseInt(btn.getAttribute('data-idx')), 1);
|
||||||
|
renderCart();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
updateTotals();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTotals() {
|
||||||
|
var itemCount = cart.reduce(function (sum, it) { return sum + it.quantity; }, 0);
|
||||||
|
var subtotal = cart.reduce(function (sum, it) { return sum + (it.quantity * it.unit_price); }, 0);
|
||||||
|
var tax = subtotal * 0.16;
|
||||||
|
var total = subtotal + tax;
|
||||||
|
|
||||||
|
document.getElementById('sum-items').textContent = itemCount;
|
||||||
|
document.getElementById('sum-subtotal').textContent = fmt(subtotal);
|
||||||
|
document.getElementById('sum-tax').textContent = fmt(tax);
|
||||||
|
document.getElementById('sum-total').textContent = fmt(total);
|
||||||
|
|
||||||
|
updateFacturarBtn();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFacturarBtn() {
|
||||||
|
document.getElementById('btn-facturar').disabled = !(selectedCustomer && cart.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Facturar
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
document.getElementById('btn-facturar').addEventListener('click', function () {
|
||||||
|
if (!selectedCustomer || cart.length === 0) return;
|
||||||
|
|
||||||
|
var btn = this;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Generando...';
|
||||||
|
|
||||||
|
var items = cart.map(function (it) {
|
||||||
|
return {
|
||||||
|
part_id: it.part_id,
|
||||||
|
aftermarket_id: it.aftermarket_id,
|
||||||
|
description: it.description,
|
||||||
|
quantity: it.quantity,
|
||||||
|
unit_cost: it.unit_cost,
|
||||||
|
margin_pct: it.margin_pct,
|
||||||
|
unit_price: Math.round(it.unit_price * 100) / 100
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
api('/api/pos/invoices', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
customer_id: selectedCustomer.id_customer,
|
||||||
|
items: items,
|
||||||
|
notes: document.getElementById('invoice-notes').value.trim()
|
||||||
|
})
|
||||||
|
}).then(function (res) {
|
||||||
|
toast('Factura ' + res.folio + ' creada por ' + fmt(res.total));
|
||||||
|
|
||||||
|
// Reset cart
|
||||||
|
cart = [];
|
||||||
|
renderCart();
|
||||||
|
document.getElementById('invoice-notes').value = '';
|
||||||
|
|
||||||
|
// Refresh customer balance
|
||||||
|
selectCustomer(selectedCustomer.id_customer);
|
||||||
|
|
||||||
|
btn.textContent = 'Facturar';
|
||||||
|
}).catch(function (err) {
|
||||||
|
toast(err.message, 'error');
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Facturar';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Init
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
renderCart();
|
||||||
|
})();
|
||||||
678
dashboard/tienda.css
Normal file
678
dashboard/tienda.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
156
dashboard/tienda.html
Normal file
156
dashboard/tienda.html
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||||
|
<title>Tienda — NEXUS AUTOPARTS</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700;800&family=Outfit:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="/shared.css">
|
||||||
|
<link rel="stylesheet" href="/tienda.css">
|
||||||
|
<link rel="manifest" crossorigin="use-credentials">
|
||||||
|
<meta name="theme-color" content="#0a0a0f">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="t-header">
|
||||||
|
<div class="t-header-left">
|
||||||
|
<div class="t-logo-mark"></div>
|
||||||
|
<div class="t-brand">
|
||||||
|
<span class="t-brand-name">NEXUS</span>
|
||||||
|
<span class="t-brand-sub">AUTOPARTS</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="t-header-center">
|
||||||
|
<div class="t-search-box">
|
||||||
|
<svg class="t-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
|
||||||
|
<input id="global-search" type="text" placeholder="Buscar parte, OEM, cliente..." autocomplete="off">
|
||||||
|
<div id="global-results" class="t-search-results"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="t-header-right">
|
||||||
|
<span class="t-clock" id="clock"></span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- Main Grid -->
|
||||||
|
<main class="t-main">
|
||||||
|
<!-- KPI Row -->
|
||||||
|
<section class="t-kpi-row">
|
||||||
|
<div class="t-kpi" data-color="accent">
|
||||||
|
<div class="t-kpi-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2v20M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="t-kpi-data">
|
||||||
|
<span class="t-kpi-value" id="kpi-sales-today">$0</span>
|
||||||
|
<span class="t-kpi-label">Ventas hoy</span>
|
||||||
|
</div>
|
||||||
|
<span class="t-kpi-count" id="kpi-sales-count">0 facturas</span>
|
||||||
|
</div>
|
||||||
|
<div class="t-kpi" data-color="success">
|
||||||
|
<div class="t-kpi-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="t-kpi-data">
|
||||||
|
<span class="t-kpi-value" id="kpi-month">$0</span>
|
||||||
|
<span class="t-kpi-label">Ventas del mes</span>
|
||||||
|
</div>
|
||||||
|
<span class="t-kpi-count" id="kpi-month-count">0 facturas</span>
|
||||||
|
</div>
|
||||||
|
<div class="t-kpi" data-color="info">
|
||||||
|
<div class="t-kpi-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4-4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="t-kpi-data">
|
||||||
|
<span class="t-kpi-value" id="kpi-customers">0</span>
|
||||||
|
<span class="t-kpi-label">Clientes activos</span>
|
||||||
|
</div>
|
||||||
|
<span class="t-kpi-count" id="kpi-parts-count">0 partes</span>
|
||||||
|
</div>
|
||||||
|
<div class="t-kpi" data-color="warning">
|
||||||
|
<div class="t-kpi-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="1" y="4" width="22" height="16" rx="2"/><path d="M1 10h22"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="t-kpi-data">
|
||||||
|
<span class="t-kpi-value" id="kpi-pending">$0</span>
|
||||||
|
<span class="t-kpi-label">Por cobrar</span>
|
||||||
|
</div>
|
||||||
|
<span class="t-kpi-count" id="kpi-pending-count">0 facturas</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Content Grid: 2 columns -->
|
||||||
|
<section class="t-content">
|
||||||
|
<!-- Left column -->
|
||||||
|
<div class="t-col">
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="t-card t-actions-card">
|
||||||
|
<h2 class="t-card-title">Acciones</h2>
|
||||||
|
<div class="t-actions-grid">
|
||||||
|
<a href="/pos" class="t-action" data-color="accent">
|
||||||
|
<div class="t-action-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
|
||||||
|
</div>
|
||||||
|
<span>Nueva Venta</span>
|
||||||
|
</a>
|
||||||
|
<a href="/cuentas" class="t-action" data-color="info">
|
||||||
|
<div class="t-action-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 00-4-4H5a4 4 0 00-4-4v2"/><circle cx="8.5" cy="7" r="4"/><path d="M20 8v6M23 11h-6"/></svg>
|
||||||
|
</div>
|
||||||
|
<span>Cuentas</span>
|
||||||
|
</a>
|
||||||
|
<a href="/captura" class="t-action" data-color="success">
|
||||||
|
<div class="t-action-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
||||||
|
</div>
|
||||||
|
<span>Captura</span>
|
||||||
|
</a>
|
||||||
|
<a href="/" class="t-action" data-color="warning">
|
||||||
|
<div class="t-action-icon">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
|
||||||
|
</div>
|
||||||
|
<span>Catalogo</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top Debtors -->
|
||||||
|
<div class="t-card">
|
||||||
|
<h2 class="t-card-title">Cuentas pendientes</h2>
|
||||||
|
<div id="debtors-list" class="t-debtors-list">
|
||||||
|
<div class="t-empty">Sin cuentas pendientes</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right column -->
|
||||||
|
<div class="t-col">
|
||||||
|
<!-- Recent Invoices -->
|
||||||
|
<div class="t-card t-card-full">
|
||||||
|
<div class="t-card-header">
|
||||||
|
<h2 class="t-card-title">Ultimas facturas</h2>
|
||||||
|
<a href="/cuentas" class="t-see-all">Ver todas</a>
|
||||||
|
</div>
|
||||||
|
<div id="recent-invoices" class="t-invoice-list">
|
||||||
|
<div class="t-empty">Sin facturas recientes</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cobros de hoy -->
|
||||||
|
<div class="t-card">
|
||||||
|
<div class="t-card-header">
|
||||||
|
<h2 class="t-card-title">Cobros de hoy</h2>
|
||||||
|
</div>
|
||||||
|
<div class="t-today-payments">
|
||||||
|
<div class="t-today-amount" id="payments-today-amount">$0.00</div>
|
||||||
|
<div class="t-today-count" id="payments-today-count">0 pagos registrados</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="/tienda.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
187
dashboard/tienda.js
Normal file
187
dashboard/tienda.js
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
/**
|
||||||
|
* tienda.js — Store / Tablet dashboard logic for Nexus Autoparts
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var API = '';
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Utility
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
function fmt(n) {
|
||||||
|
return '$' + (parseFloat(n) || 0).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(s) {
|
||||||
|
if (!s) return '';
|
||||||
|
var d = document.createElement('div');
|
||||||
|
d.textContent = s;
|
||||||
|
return d.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Clock
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
function updateClock() {
|
||||||
|
var now = new Date();
|
||||||
|
var h = now.getHours();
|
||||||
|
var m = String(now.getMinutes()).padStart(2, '0');
|
||||||
|
var ampm = h >= 12 ? 'PM' : 'AM';
|
||||||
|
h = h % 12 || 12;
|
||||||
|
document.getElementById('clock').textContent = h + ':' + m + ' ' + ampm;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateClock();
|
||||||
|
setInterval(updateClock, 30000);
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Load Dashboard Stats
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
function loadStats() {
|
||||||
|
fetch(API + '/api/tienda/stats')
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (d) {
|
||||||
|
var st = d.sales_today || {};
|
||||||
|
var sm = d.sales_month || {};
|
||||||
|
var pt = d.payments_today || {};
|
||||||
|
|
||||||
|
// KPIs
|
||||||
|
document.getElementById('kpi-sales-today').textContent = fmt(st.total);
|
||||||
|
document.getElementById('kpi-sales-count').textContent = (st.count || 0) + ' facturas';
|
||||||
|
document.getElementById('kpi-month').textContent = fmt(sm.total);
|
||||||
|
document.getElementById('kpi-month-count').textContent = (sm.count || 0) + ' facturas';
|
||||||
|
document.getElementById('kpi-customers').textContent = d.total_customers || 0;
|
||||||
|
document.getElementById('kpi-parts-count').textContent = (d.total_parts || 0) + ' partes';
|
||||||
|
document.getElementById('kpi-pending').textContent = fmt(d.pending_balance || 0);
|
||||||
|
document.getElementById('kpi-pending-count').textContent = (d.pending_invoices || 0) + ' facturas';
|
||||||
|
|
||||||
|
// Today's payments
|
||||||
|
document.getElementById('payments-today-amount').textContent = fmt(pt.total);
|
||||||
|
document.getElementById('payments-today-count').textContent = (pt.count || 0) + ' pagos registrados';
|
||||||
|
|
||||||
|
// Top debtors
|
||||||
|
renderDebtors(d.top_debtors || []);
|
||||||
|
|
||||||
|
// Recent invoices
|
||||||
|
renderInvoices(d.recent_invoices || []);
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
console.error('Error loading stats:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Render Debtors
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
function renderDebtors(debtors) {
|
||||||
|
var el = document.getElementById('debtors-list');
|
||||||
|
|
||||||
|
if (debtors.length === 0) {
|
||||||
|
el.innerHTML = '<div class="t-empty">Sin cuentas pendientes</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.innerHTML = debtors.map(function (d) {
|
||||||
|
var limitPct = d.credit_limit > 0 ? Math.round(d.balance / d.credit_limit * 100) : 0;
|
||||||
|
return '<a href="/cuentas" class="t-debtor">'
|
||||||
|
+ '<div>'
|
||||||
|
+ '<div class="t-debtor-name">' + esc(d.name) + '</div>'
|
||||||
|
+ (d.credit_limit > 0 ? '<div class="t-debtor-invoices">' + limitPct + '% de l\u00edmite</div>' : '')
|
||||||
|
+ '</div>'
|
||||||
|
+ '<span class="t-debtor-amount">' + fmt(d.balance) + '</span>'
|
||||||
|
+ '</a>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Render Recent Invoices
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
function renderInvoices(invoices) {
|
||||||
|
var el = document.getElementById('recent-invoices');
|
||||||
|
|
||||||
|
if (invoices.length === 0) {
|
||||||
|
el.innerHTML = '<div class="t-empty">Sin facturas recientes</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.innerHTML = invoices.map(function (inv) {
|
||||||
|
var statusClass = inv.status || 'pending';
|
||||||
|
var statusLabel = { pending: 'Pendiente', paid: 'Pagada', partial: 'Parcial', cancelled: 'Cancelada' };
|
||||||
|
return '<div class="t-invoice">'
|
||||||
|
+ '<div class="t-invoice-left">'
|
||||||
|
+ '<span class="t-invoice-folio">' + esc(inv.folio) + '</span>'
|
||||||
|
+ '<span class="t-invoice-customer">' + esc(inv.customer_name) + '</span>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div class="t-invoice-right">'
|
||||||
|
+ '<span class="t-invoice-total">' + fmt(inv.total) + '</span>'
|
||||||
|
+ '<span class="t-invoice-status ' + statusClass + '">' + (statusLabel[statusClass] || statusClass) + '</span>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '</div>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Global Search
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
var searchTimer = null;
|
||||||
|
var searchInput = document.getElementById('global-search');
|
||||||
|
var searchResults = document.getElementById('global-results');
|
||||||
|
|
||||||
|
if (searchInput) {
|
||||||
|
searchInput.addEventListener('input', function () {
|
||||||
|
clearTimeout(searchTimer);
|
||||||
|
var q = this.value.trim();
|
||||||
|
if (q.length < 2) {
|
||||||
|
searchResults.classList.remove('active');
|
||||||
|
searchResults.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchTimer = setTimeout(function () {
|
||||||
|
fetch(API + '/api/pos/search-parts?q=' + encodeURIComponent(q))
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (results) {
|
||||||
|
if (results.length === 0) {
|
||||||
|
searchResults.innerHTML = '<div style="padding:0.8rem;color:var(--text-secondary);font-size:0.85rem">Sin resultados para "' + esc(q) + '"</div>';
|
||||||
|
} else {
|
||||||
|
searchResults.innerHTML = results.slice(0, 8).map(function (p) {
|
||||||
|
return '<div class="t-search-result-item">'
|
||||||
|
+ '<div>'
|
||||||
|
+ '<span class="sri-number">' + esc(p.oem_part_number) + '</span>'
|
||||||
|
+ '<span class="sri-name">' + esc(p.name_part) + '</span>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '</div>';
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
searchResults.classList.add('active');
|
||||||
|
});
|
||||||
|
}, 250);
|
||||||
|
});
|
||||||
|
|
||||||
|
searchInput.addEventListener('blur', function () {
|
||||||
|
setTimeout(function () { searchResults.classList.remove('active'); }, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
searchInput.addEventListener('focus', function () {
|
||||||
|
if (searchResults.innerHTML.trim()) {
|
||||||
|
searchResults.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================================================================
|
||||||
|
// Init
|
||||||
|
// ================================================================
|
||||||
|
|
||||||
|
loadStats();
|
||||||
|
|
||||||
|
// Auto-refresh every 2 minutes
|
||||||
|
setInterval(loadStats, 120000);
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user