Add multi-item sales
This commit is contained in:
+4
-2
@@ -82,9 +82,11 @@ function receiveForm() {
|
||||
}
|
||||
function sellForm(id="") {
|
||||
const available=state.products.filter(p=>p.status==="available");
|
||||
openModal(`<h2>Record a sale</h2><p>Enter the customer, payment, fulfillment, and warranty details.</p><form id="sellForm"><label>Product<select name="productId" required>${available.map(p=>`<option value="${p.id}" ${p.id===id?"selected":""}>${esc(p.model)} — ${esc(primaryId(p))}</option>`).join("")}</select></label>${customerFields()}<div class="form-row"><label>Payment method<select name="paymentMethod" required><option value="">Choose a method</option><option>Cash</option><option>Venmo</option><option>PayPal</option></select></label><label>Payment reference (optional)<input name="paymentReference" placeholder="Transaction ID or note"></label></div><div class="form-row"><label>Sale price<input name="salePrice" type="number" min="0" step=".01"></label><label>Date sold<input name="soldAt" type="date" value="${today()}" required></label></div><div class="form-row"><label>Delivery method<select name="fulfillmentMethod" required><option value="">Choose a method</option><option>Shipped</option><option>Dropped Off</option><option>Installed At</option><option>Meet</option></select></label><label>Warranty<select name="warrantyPresetId" required>${warrantyOptions()}</select></label></div><div class="fulfillment-fields"></div><label>Transaction notes (optional)<textarea name="saleNotes" rows="3" placeholder="Additional notes about this sale"></textarea></label><div class="form-actions"><button type="button" class="secondary" data-cancel>Cancel</button><button class="primary">Complete sale</button></div></form>`);
|
||||
if(!available.length){openModal('<h2>No available inventory</h2><p>Receive a product before recording a sale.</p><div class="form-actions"><button class="primary" data-cancel>Close</button></div>');$("[data-cancel]").onclick=closeModal;return;}
|
||||
openModal(`<h2>Record a sale</h2><p>Select one or more products. Customer, payment, fulfillment, warranty, and notes apply to every selected item.</p><form id="sellForm"><fieldset class="sale-items"><legend>Products</legend><div class="sale-item-list">${available.map(p=>`<label class="sale-item"><input type="checkbox" name="productIds" value="${p.id}" ${p.id===id?"checked":""}><span><strong>${esc(p.model)}</strong><small>${esc(primaryId(p))}</small></span><span class="sale-item-price">Sale price<input type="number" min="0" step=".01" inputmode="decimal" data-price="${p.id}" aria-label="Sale price for ${esc(p.model)} ${esc(primaryId(p))}" ${p.id===id?"":"disabled"}></span></label>`).join("")}</div><div class="sale-summary" aria-live="polite"><span><strong data-selected-count>0</strong> selected</span><strong data-sale-total>${money.format(0)}</strong></div></fieldset>${customerFields()}<div class="form-row"><label>Payment method<select name="paymentMethod" required><option value="">Choose a method</option><option>Cash</option><option>Venmo</option><option>PayPal</option></select></label><label>Payment reference (optional)<input name="paymentReference" placeholder="Transaction ID or note"></label></div><label>Date sold<input name="soldAt" type="date" value="${today()}" required></label><div class="form-row"><label>Delivery method<select name="fulfillmentMethod" required><option value="">Choose a method</option><option>Shipped</option><option>Dropped Off</option><option>Installed At</option><option>Meet</option></select></label><label>Warranty<select name="warrantyPresetId" required>${warrantyOptions()}</select></label></div><div class="fulfillment-fields"></div><label>Transaction notes (optional)<textarea name="saleNotes" rows="3" placeholder="Additional notes about this sale"></textarea></label><div class="form-actions"><button type="button" class="secondary" data-cancel>Cancel</button><button class="primary">Complete sale</button></div></form>`);
|
||||
bindFulfillment($("#sellForm"));
|
||||
bindCustomer($("#sellForm"));$("#sellForm").onsubmit=async e=>{ e.preventDefault();const button=e.submitter;button.disabled=true;const data=Object.fromEntries(new FormData(e.currentTarget)), productId=data.productId;delete data.productId;try{await change(`/api/products/${encodeURIComponent(productId)}/sell`,"POST",data,"Sale recorded.");}finally{button.disabled=false} }; $("[data-cancel]").onclick=closeModal;
|
||||
const form=$("#sellForm"),checks=[...form.querySelectorAll('[name="productIds"]')],drawTotal=()=>{let count=0,total=0;checks.forEach(check=>{const price=form.querySelector(`[data-price="${check.value}"]`);price.disabled=!check.checked;if(check.checked){count++;total+=Number(price.value)||0}});form.querySelector("[data-selected-count]").textContent=count;form.querySelector("[data-sale-total]").textContent=money.format(total);form.querySelector('.form-actions .primary').textContent=count>1?`Complete sale (${count} items)`:"Complete sale";};checks.forEach(check=>check.onchange=drawTotal);form.querySelectorAll("[data-price]").forEach(input=>input.oninput=drawTotal);drawTotal();
|
||||
bindCustomer(form);form.onsubmit=async e=>{e.preventDefault();const button=e.submitter,selected=checks.filter(x=>x.checked);if(!selected.length){toast("Select at least one product.");return;}button.disabled=true;const data=Object.fromEntries(new FormData(form));delete data.productIds;data.items=selected.map(check=>({productId:check.value,salePrice:Number(form.querySelector(`[data-price="${check.value}"]`).value)||0}));try{await change("/api/sales","POST",data,`${selected.length} ${selected.length===1?"item":"items"} sold.`);}finally{button.disabled=false}};$("[data-cancel]").onclick=closeModal;
|
||||
}
|
||||
async function submitForm(e,path,message,method="POST"){ e.preventDefault(); await change(path,method,Object.fromEntries(new FormData(e.currentTarget)),message); }
|
||||
async function change(path,method,body,message){ try{ await api(path,{method,body:body?JSON.stringify(body):undefined}); closeModal(); await load(); toast(message); }catch(e){ toast(e.message); } }
|
||||
|
||||
@@ -7,4 +7,5 @@
|
||||
.auth-pending #appShell,.auth-required #appShell{display:none}.role-admin #authScreen,.role-readonly #authScreen{display:none}.auth-screen{min-height:100vh;display:grid;place-items:center;padding:24px;background:linear-gradient(145deg,#eef4ff,#f8fafc)}.auth-card{width:min(430px,100%);background:#fff;border:1px solid #dfe5ef;border-radius:18px;box-shadow:0 22px 60px #17203320;padding:34px}.auth-brand{margin-bottom:28px}.auth-card h1{margin:0 0 7px}.auth-card>div>p{color:#68758b}.auth-submit{width:100%;margin-top:14px}.auth-message{background:#fff0f0;color:#a51d1d;border:1px solid #fecaca;border-radius:8px;padding:10px 12px;margin:14px 0}.auth-logout{width:100%;margin-top:10px}.user-menu{display:flex;align-items:center;gap:10px;border-left:1px solid #e3e7ef;padding-left:18px}.user-menu span{display:grid;font-size:13px}.user-menu small{color:#758197}.admin-section{display:grid;gap:16px;border-bottom:1px solid #e3e7ef;padding-bottom:28px}.admin-section:last-child{border-bottom:0}.inline-user-form{display:grid;grid-template-columns:1fr 1fr 180px auto;gap:10px;align-items:end}.inline-user-form label{margin:0}.user-list,.audit-list{border:1px solid #e3e7ef;border-radius:10px;overflow:hidden}.user-list article{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:14px;border-bottom:1px solid #edf0f5}.user-list article:last-child{border-bottom:0}.user-list small{display:block;color:#7b8799;margin-top:4px}.user-list article>div:last-child{display:flex;gap:7px;flex-wrap:wrap}.delete-backup{background:#fff0f0;color:#c62828}.audit-list{max-height:420px;overflow:auto}.audit-list article{display:grid;grid-template-columns:140px 180px 1fr;gap:12px;padding:10px 13px;border-bottom:1px solid #edf0f5;font-size:13px}.audit-list span{text-transform:capitalize}.audit-list small{color:#7b8799}@media(max-width:900px){.inline-user-form{grid-template-columns:1fr 1fr}.user-list article{align-items:flex-start;flex-direction:column}.audit-list article{grid-template-columns:1fr}.user-menu span{display:none}}@media(max-width:600px){.inline-user-form{grid-template-columns:1fr}.user-menu{padding-left:5px}.user-menu button{font-size:12px;padding:8px}.auth-card{padding:25px}}
|
||||
.inline-model-form{display:grid;grid-template-columns:minmax(220px,1fr) auto;gap:10px;align-items:end}.inline-model-form label{margin:0}.model-list{border:1px solid #e3e7ef;border-radius:10px;overflow:hidden}.model-list article{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:14px;border-bottom:1px solid #edf0f5}.model-list article:last-child{border-bottom:0}.model-summary>div{display:flex;align-items:center;gap:9px}.model-summary small{display:block;color:#7b8799;margin-top:5px}.model-status{display:inline-flex;border-radius:99px;padding:3px 8px;font-size:11px;font-weight:750}.model-status.active{background:#dcfce7;color:#166534}.model-status.archived{background:#eef1f5;color:#526071}.model-actions{display:flex;gap:7px;flex-wrap:wrap}@media(max-width:760px){.inline-model-form{grid-template-columns:1fr}.model-list article{align-items:stretch;flex-direction:column}.model-actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.model-actions button{text-align:center}}
|
||||
.conditional-fields{margin:2px 0 14px;padding:16px;border:1px solid #e3e7ef;border-radius:11px;background:#f8fafc}.conditional-fields h3{margin:0 0 12px}.conditional-hint{margin:4px 0 16px;padding:13px;border:1px dashed #d7deea;border-radius:9px;color:#68758b}.field-help{display:block;margin-top:-8px;color:#68758b}.warranty-pill{display:inline-flex;align-items:center;border-radius:99px;padding:5px 9px;font-size:12px;font-weight:750;white-space:nowrap}.warranty-pill.in-warranty{background:#dcfce7;color:#166534}.warranty-pill.expired{background:#fee2e2;color:#b91c1c}.warranty-pill.neutral{background:#eef1f5;color:#526071}.inline-warranty-form{display:grid;grid-template-columns:minmax(180px,1fr) 130px 150px auto;gap:10px;align-items:end}.inline-warranty-form label{margin:0}.warranty-list{border:1px solid #e3e7ef;border-radius:10px;overflow:hidden}.warranty-list article{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:14px;border-bottom:1px solid #edf0f5}.warranty-list article:last-child{border-bottom:0}@media(max-width:760px){.inline-warranty-form{grid-template-columns:1fr}.warranty-list article{align-items:stretch;flex-direction:column}}
|
||||
.sale-items{margin:18px 0;padding:0;border:1px solid #d7deea;border-radius:11px;overflow:hidden}.sale-items legend{margin-left:12px;padding:0 6px;font-size:13px;font-weight:750}.sale-item-list{max-height:250px;overflow:auto}.sale-item{display:grid;grid-template-columns:auto minmax(0,1fr) 130px;align-items:center;gap:11px;margin:0;padding:11px 13px;border-bottom:1px solid #edf0f5}.sale-item>input{width:18px;height:18px}.sale-item small{display:block;margin-top:3px;color:#68758b}.sale-item-price{font-size:12px}.sale-item-price input{margin-top:4px;text-align:right}.sale-summary{display:flex;justify-content:space-between;align-items:center;padding:12px 14px;background:#f8fafc}.sale-summary span{color:#68758b}@media(max-width:520px){.sale-item{grid-template-columns:auto minmax(0,1fr)}.sale-item-price{grid-column:2}.sale-item-list{max-height:310px}}
|
||||
.filter-bar{display:flex;align-items:end;gap:9px;flex-wrap:wrap;padding:12px 16px;border-bottom:1px solid #e3e7ef}.filter-bar label{margin:0;min-width:125px;font-size:11px}.filter-bar select,.filter-bar input{padding:8px 9px;font-size:13px}.filter-bar .button,.filter-bar button{padding:9px 12px}.export-button{text-decoration:none}.customer-match{min-height:18px;margin:-7px 0 10px!important;color:#1d4ed8!important;font-size:12px}.diagnostic-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}.diagnostic-grid article{display:grid;gap:4px;padding:14px;border:1px solid #e3e7ef;border-radius:10px}.diagnostic-grid span,.diagnostic-grid small{color:#68758b}.diagnostic-grid strong{font-size:18px}.automatic-backups{display:grid;gap:12px;padding:16px;border:1px solid #e3e7ef;border-radius:10px}.automatic-backups h3,.automatic-backups p{margin:0}.backup-schedule{display:grid;grid-template-columns:minmax(190px,1.3fr) repeat(4,minmax(130px,1fr)) auto;gap:10px;align-items:end}.backup-schedule label{margin:0}.weekly-field[hidden]{display:none!important}.check-label{display:flex!important;align-items:center;gap:8px;min-height:42px}.check-label input{width:auto}.backup-schedule-status{display:flex;gap:18px;flex-wrap:wrap;font-size:13px}.export-links{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.export-links .button{padding:8px 11px;text-decoration:none}@media(max-width:1100px){.backup-schedule{grid-template-columns:1fr 1fr 1fr}}@media(max-width:900px){.diagnostic-grid{grid-template-columns:1fr 1fr}.backup-schedule{grid-template-columns:1fr 1fr}}@media(max-width:600px){.filter-bar{align-items:stretch}.filter-bar label,.filter-bar .button,.filter-bar button{width:100%}.diagnostic-grid,.backup-schedule{grid-template-columns:1fr}}
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ th{background:var(--surface-table);color:var(--muted-2)}th,td,.records nav,.back
|
||||
.pill{background:var(--green-soft);color:var(--green)}.detail-card{background:var(--surface-soft);border-color:var(--border)}
|
||||
.customer-notes article,.backup-list,.user-list,.model-list,.warranty-list,.audit-list{border-color:var(--border)}
|
||||
.model-list article{border-color:var(--border)}.model-summary small{color:var(--muted)}.model-status.active{background:var(--green-soft);color:var(--green)}.model-status.archived{background:var(--surface-soft);color:var(--muted)}
|
||||
.warranty-list article,.conditional-fields{border-color:var(--border)}.conditional-fields{background:var(--surface-soft)}.conditional-hint{border-color:var(--border-strong);color:var(--muted)}.field-help{color:var(--muted)}.warranty-pill.in-warranty{background:var(--green-soft);color:var(--green)}.warranty-pill.expired{background:var(--danger-soft);color:var(--danger)}.warranty-pill.neutral{background:var(--surface-soft);color:var(--muted)}
|
||||
.warranty-list article,.conditional-fields,.sale-items,.sale-item{border-color:var(--border)}.conditional-fields,.sale-summary{background:var(--surface-soft)}.sale-item small,.sale-summary span,.conditional-hint{color:var(--muted)}.conditional-hint{border-color:var(--border-strong)}.field-help{color:var(--muted)}.warranty-pill.in-warranty{background:var(--green-soft);color:var(--green)}.warranty-pill.expired{background:var(--danger-soft);color:var(--danger)}.warranty-pill.neutral{background:var(--surface-soft);color:var(--muted)}
|
||||
.filter-bar{border-color:var(--border);background:var(--surface-soft)}.filter-bar .button{background:var(--surface);border:1px solid var(--border-strong);color:var(--text)}.customer-match{color:var(--blue-text)!important}.diagnostic-grid article,.automatic-backups{background:var(--surface-soft);border-color:var(--border)}.diagnostic-grid span,.diagnostic-grid small,.automatic-backups p,.backup-schedule-status{color:var(--muted)}.export-links .button{background:var(--surface-soft);border:1px solid var(--border-strong);color:var(--text)}
|
||||
.note-category{background:var(--blue-soft);color:var(--blue-text)}.backup-warning{background:var(--warning-soft);border-color:color-mix(in srgb,var(--warning) 40%,transparent);color:var(--warning)}
|
||||
.auth-screen{background:linear-gradient(145deg,color-mix(in srgb,var(--blue) 10%,var(--page)),var(--page))}.auth-card{box-shadow:0 22px 60px var(--shadow)}
|
||||
|
||||
Reference in New Issue
Block a user