feat: Minha conta with sign-in, overview, orders and account details
All checks were successful
Build and deploy / Validate source (push) Successful in 6s
Build and deploy / Integration suite on a real stack (push) Successful in 3m7s
Build and deploy / Secret scan and release gate (push) Successful in 10s
Build and deploy / Publish images (push) Successful in 1m6s

The order page becomes the customer's area, one page at four addresses:

- /conta/entrar: sign in or create an account, side by side. "Esqueci
  minha senha" points to the Dropstar WhatsApp until e-mail can be sent.
- /conta: the counts of orders waiting for payment, in production, in
  correction and finished, and the latest one.
- /conta/pedidos: every order and the cart waiting for payment in one list,
  newest first, filtered by group, order number and period, ten per page.
  The cart shows "Aguardando pagamento" and its "Pagar" goes to the PIX
  page when a PIX code is open. An order opens in place with its progress,
  items, delivery, history, files and the correction form. A guest sees
  the orders paid in this browser.
- /conta/dados: WhatsApp and a saved delivery address (the CNPJ is locked),
  e-mail and password changes, both confirmed with the current password;
  a password change signs the other devices out.

The cart fills in the account's details and saved address. New API routes
for the details, and the order list takes filters and pages and returns
the counts; only the newest unpaid quote whose files still exist is listed.
The Site's "Minha conta" and "Ver meus pedidos" point to the new addresses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Cauê Faleiros
2026-09-30 16:57:37 -03:00
parent 223854e392
commit 24ee4e9eab
12 changed files with 631 additions and 184 deletions

View File

@@ -2,7 +2,9 @@
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from fastapi import APIRouter, Depends, HTTPException, Request, Response from typing import Literal
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response
from psycopg.errors import UniqueViolation from psycopg.errors import UniqueViolation
from psycopg.types.json import Jsonb from psycopg.types.json import Jsonb
@@ -10,7 +12,7 @@ from ..core import db
from ..artwork import submit_files from ..artwork import submit_files
from ..core.auth import (DUMMY_PASSWORD_HASH, audit, client_ip, new_session, owner, from ..core.auth import (DUMMY_PASSWORD_HASH, audit, client_ip, new_session, owner,
login_failed, password_hash, password_matches, session_row, throttle, transfer_guest) login_failed, password_hash, password_matches, session_row, throttle, transfer_guest)
from ..core.models import ArtworkSubmission, Login, Register from ..core.models import ArtworkSubmission, EmailChange, Login, PasswordChange, ProfileUpdate, Register
from ..runtime import STATES, file_rows, owned_order, storage from ..runtime import STATES, file_rows, owned_order, storage
from ..scanning import require_clean from ..scanning import require_clean
@@ -86,13 +88,112 @@ def me(identity=Depends(owner)):
row = c.execute('SELECT profile FROM dtf_local.accounts WHERE id=%s', (identity,)).fetchone() row = c.execute('SELECT profile FROM dtf_local.accounts WHERE id=%s', (identity,)).fetchone()
return {'customer': row['profile'] if row else None} return {'customer': row['profile'] if row else None}
@router.get('/api/customer/orders') def account_row(c, identity, lock=False):
def orders(identity=Depends(owner)): row = c.execute('SELECT * FROM dtf_local.accounts WHERE id=%s' + (' FOR UPDATE' if lock else ''), (identity,)).fetchone()
if not row:
raise HTTPException(401, 'Entre na sua conta.')
return row
@router.post('/api/account/profile')
def update_profile(body: ProfileUpdate, identity=Depends(owner)):
"""WhatsApp and the saved delivery address. The CNPJ is the account's and
does not change; the e-mail is the login and changes on its own route."""
with db.connect() as c: with db.connect() as c:
rows = c.execute('SELECT id,number,state,version,snapshot,created_at,updated_at FROM dtf_local.orders WHERE owner=%s ORDER BY created_at DESC', (identity,)).fetchall() row = account_row(c, identity, lock=True)
quotes = c.execute('''SELECT q.id,q.approved,q.approved_at,q.created_at FROM dtf_local.quotes q profile = {**row['profile'], 'zap': body.zap,
LEFT JOIN dtf_local.orders o ON o.quote_id=q.id WHERE q.owner=%s AND o.id IS NULL ORDER BY q.created_at DESC''', (identity,)).fetchall() 'address': body.address.model_dump() if body.address else None}
return {'orders': rows, 'quotes': quotes, 'states': STATES} c.execute('UPDATE dtf_local.accounts SET profile=%s WHERE id=%s', (Jsonb(profile), identity))
audit('account_profile_updated', account=str(identity))
return {'customer': profile}
@router.post('/api/account/email')
def change_email(body: EmailChange, request: Request, identity=Depends(owner)):
with db.connect() as c:
row = account_row(c, identity)
throttle(row['email'], request)
if not password_matches(body.password, row['password_hash']):
login_failed(row['email'])
raise HTTPException(401, 'Senha incorreta.')
profile = {**row['profile'], 'mail': body.email}
try:
with db.connect() as c:
c.execute('UPDATE dtf_local.accounts SET email=%s, profile=%s WHERE id=%s', (body.email, Jsonb(profile), identity))
except UniqueViolation:
raise HTTPException(409, 'Este e-mail já é de outra conta.')
audit('account_email_changed', account=str(identity))
return {'customer': profile}
@router.post('/api/account/password')
def change_password(body: PasswordChange, request: Request, identity=Depends(owner)):
with db.connect() as c:
row = account_row(c, identity)
throttle(row['email'], request)
if not password_matches(body.current, row['password_hash']):
login_failed(row['email'])
raise HTTPException(401, 'Senha atual incorreta.')
session = current(request)
with db.connect() as c:
c.execute('UPDATE dtf_local.accounts SET password_hash=%s WHERE id=%s', (password_hash(body.new), identity))
# Other devices signed in with the old password are signed out.
c.execute('DELETE FROM dtf_local.sessions WHERE owner=%s AND id<>%s', (identity, session['id']))
audit('account_password_changed', account=str(identity))
return {'ok': True}
# The order list's groups, as the customer filters them.
GROUPS = {'pay': 'Aguardando pagamento', 'prod': 'Em produção', 'cor': 'Correção', 'fin': 'Finalizados'}
ENTRIES = '''WITH live_quote AS (
-- The cart waiting for payment: the newest unpaid quote whose files still
-- exist. An older one was replaced when the cart changed, and one whose
-- files are gone can no longer be paid.
SELECT q.id, q.created_at, q.approved FROM dtf_local.quotes q
LEFT JOIN dtf_local.orders o ON o.quote_id=q.id
WHERE q.owner=%(me)s AND o.id IS NULL
AND NOT EXISTS (SELECT 1 FROM jsonb_array_elements(q.draft->'items') i
CROSS JOIN LATERAL jsonb_array_elements_text(i->'uploads') u(uid)
JOIN dtf_local.uploads up ON up.id=u.uid::uuid
WHERE up.purged_at IS NOT NULL OR up.expires_at<=now())
ORDER BY q.created_at DESC LIMIT 1
), entries AS (
SELECT 'order' AS kind, o.id, o.number, o.state,
CASE WHEN o.state='fin' THEN 'fin' WHEN o.state='cor' THEN 'cor' ELSE 'prod' END AS grp,
(o.snapshot->>'total_cents')::bigint AS total_cents, o.created_at, o.snapshot->'items' AS items
FROM dtf_local.orders o WHERE o.owner=%(me)s
UNION ALL
SELECT 'quote', q.id, NULL, CASE WHEN q.approved IS NULL THEN 'review' ELSE 'pay' END, 'pay',
(q.approved->>'total_cents')::bigint, q.created_at, COALESCE(q.approved->'items','[]'::jsonb)
FROM live_quote q
)'''
@router.get('/api/customer/orders')
def orders(identity=Depends(owner), status: Literal['all','pay','prod','cor','fin'] = 'all',
number: int | None = Query(None, ge=1), days: int = Query(0, ge=0, le=3650),
page: int = Query(1, ge=1, le=10000), size: int = Query(10, ge=1, le=50)):
"""The customer's orders and the cart waiting for payment, newest first,
filtered and a page at a time, with how many there are in each group."""
params = {'me': identity, 'grp': None if status == 'all' else status, 'number': number,
'since': datetime.now(timezone.utc) - timedelta(days=days) if days else None,
'size': size, 'offset': (page - 1) * size}
where = '''WHERE (%(grp)s::text IS NULL OR grp=%(grp)s) AND (%(number)s::int IS NULL OR number=%(number)s)
AND (%(since)s::timestamptz IS NULL OR created_at>=%(since)s)'''
with db.connect() as c:
counts = {r['grp']: r['n'] for r in c.execute(ENTRIES + ' SELECT grp, count(*) AS n FROM entries GROUP BY grp', params).fetchall()}
total = c.execute(ENTRIES + ' SELECT count(*) AS n FROM entries ' + where, params).fetchone()['n']
rows = c.execute(ENTRIES + ' SELECT * FROM entries ' + where +
' ORDER BY created_at DESC LIMIT %(size)s OFFSET %(offset)s', params).fetchall()
items = []
for r in rows:
entry = {'kind': r['kind'], 'id': r['id'], 'number': r['number'], 'state': r['state'],
'group': r['grp'], 'total_cents': r['total_cents'], 'created_at': r['created_at'],
'items': [{'mode': i['mode'], 'billed_metres': i.get('billed_metres')} for i in r['items']]}
if r['kind'] == 'quote':
# A PIX code still open is paid on its own page.
pix = c.execute('''SELECT 1 FROM dtf_local.payment_intents WHERE quote_id=%s AND method='pix'
AND status='pending' AND COALESCE((response->>'expires_at')::timestamptz > now(), false)''',
(r['id'],)).fetchone()
entry['pay_with'] = 'pix' if pix else 'page'
items.append(entry)
return {'items': items, 'total': total, 'page': page, 'size': size,
'counts': {g: counts.get(g, 0) for g in GROUPS}, 'groups': GROUPS, 'states': STATES}
@router.get('/api/customer/orders/{oid}') @router.get('/api/customer/orders/{oid}')
def detail(oid: UUID, identity=Depends(owner)): def detail(oid: UUID, identity=Depends(owner)):
@@ -103,7 +204,8 @@ def detail(oid: UUID, identity=Depends(owner)):
history = c.execute('''SELECT from_state,to_state,CASE WHEN to_state='cor' THEN reason ELSE '' END AS reason, history = c.execute('''SELECT from_state,to_state,CASE WHEN to_state='cor' THEN reason ELSE '' END AS reason,
created_at FROM dtf_local.movements WHERE order_id=%s AND NOT back ORDER BY id''', (oid,)).fetchall() created_at FROM dtf_local.movements WHERE order_id=%s AND NOT back ORDER BY id''', (oid,)).fetchall()
return {'id': row['id'], 'number': row['number'], 'state': row['state'], 'version': row['version'], return {'id': row['id'], 'number': row['number'], 'state': row['state'], 'version': row['version'],
'snapshot': row['snapshot'], 'history': history, 'files': file_rows(c,oid)} 'created_at': row['created_at'], 'snapshot': row['snapshot'], 'history': history,
'files': file_rows(c,oid)}
@router.post('/api/customer/orders/{oid}/corrections') @router.post('/api/customer/orders/{oid}/corrections')
def correction(oid: UUID, body: ArtworkSubmission, identity=Depends(owner)): def correction(oid: UUID, body: ArtworkSubmission, identity=Depends(owner)):

View File

@@ -219,6 +219,35 @@ class Register(StrictModel):
customer: Customer customer: Customer
password: str = Field(min_length=12, max_length=128) password: str = Field(min_length=12, max_length=128)
class ProfileUpdate(StrictModel):
"""What a customer may change about their account; the CNPJ is not among it."""
zap: str
address: Destination | None = None
@field_validator('zap')
@classmethod
def phone_valid(cls, value):
digits = re.sub(r'\D', '', value)
if len(digits) not in (10,11) or not digits.isascii():
raise ValueError('Invalid phone')
return digits
class EmailChange(StrictModel):
email: str = Field(max_length=254)
password: str = Field(min_length=1, max_length=128)
@field_validator('email')
@classmethod
def email_valid(cls, value):
value = value.strip().lower()
if not re.fullmatch(r'[^\s@]+@[^\s@]+\.[a-zA-Z]{2,}', value):
raise ValueError('Invalid email')
return value
class PasswordChange(StrictModel):
current: str = Field(min_length=1, max_length=128)
new: str = Field(min_length=12, max_length=128)
class Login(StrictModel): class Login(StrictModel):
email: str = Field(min_length=3, max_length=254) email: str = Field(min_length=3, max_length=254)
password: str = Field(min_length=1, max_length=128) password: str = Field(min_length=1, max_length=128)

View File

@@ -53,6 +53,11 @@ server {
expires -1; expires -1;
try_files /index.html =404; try_files /index.html =404;
} }
# The customer's area: one page, which shows the right part for each address.
location ~ ^/conta(/(entrar|pedidos|dados))?/?$ {
expires -1;
try_files /portal.html =404;
}
location / { location / {
expires -1; expires -1;
try_files $uri $uri/ =404; try_files $uri $uri/ =404;

View File

@@ -23,6 +23,10 @@ server {
location ~ ^/(arquivo-por-metro|artes-avulsas|uv-arquivo-por-metro|uv-artes-avulsas|carrinho|pagamento|pagamento/pix)/?$ { location ~ ^/(arquivo-por-metro|artes-avulsas|uv-arquivo-por-metro|uv-artes-avulsas|carrinho|pagamento|pagamento/pix)/?$ {
try_files /index.html =404; try_files /index.html =404;
} }
# The customer's area: one page, which shows the right part for each address.
location ~ ^/conta(/(entrar|pedidos|dados))?/?$ {
try_files /portal.html =404;
}
location / { try_files $uri $uri/ =404; } location / { try_files $uri $uri/ =404; }
} }
server { server {

View File

@@ -192,14 +192,20 @@ try{
await waitFor(()=>site.eval('window.scrollY===0'),'screenshot scroll position'); await waitFor(()=>site.eval('window.scrollY===0'),'screenshot scroll position');
await site.screenshot('output/local/site.png'); await site.screenshot('output/local/site.png');
await kanban.screenshot('output/local/kanban.png'); await kanban.screenshot('output/local/kanban.png');
const portal=await page(siteOrigin+'/portal.html?order='+oid); // A guest sees the order paid in this browser; creating an account keeps it.
const portal=await page(siteOrigin+'/conta/pedidos?order='+oid);
await waitFor(async()=> (await portal.text()).includes('Finalizado'),'customer order tracking'); await waitFor(async()=> (await portal.text()).includes('Finalizado'),'customer order tracking');
await waitFor(()=>portal.eval('document.querySelector("#order-'+oid+' .detalhe")?.hidden===false'),'order from the link opened');
await portal.click('#navEntrar');
await waitFor(()=>portal.eval('location.pathname==="/conta/entrar" && !document.getElementById("auth").hidden'),'sign-in page');
await portal.fill('#cnpj','11222333000181');await portal.fill('#phone','11999999999'); await portal.fill('#cnpj','11222333000181');await portal.fill('#phone','11999999999');
await portal.fill('#register-email','browser-'+Date.now()+'@example.test'); await portal.fill('#register-email','browser-'+Date.now()+'@example.test');
await portal.fill('#register-password','local-browser-password-123'); await portal.fill('#register-password','local-browser-password-123');
await portal.eval('document.getElementById("register").requestSubmit()'); await portal.eval('document.getElementById("register").requestSubmit()');
await waitFor(()=>portal.eval('document.getElementById("auth").hidden'),'customer registration'); await waitFor(()=>portal.eval('document.getElementById("auth").hidden && location.pathname==="/conta"'),'customer registration');
assert.ok((await portal.text()).includes('Finalizado')); await waitFor(async()=> (await portal.text()).includes('Finalizado'),'overview shows the last order');
await portal.click('[data-view="dados"]');
await waitFor(()=>portal.eval('location.pathname==="/conta/dados" && document.getElementById("dCnpj").value==="11.222.333/0001-81"'),'account details');
await portal.screenshot('output/local/portal.png'); await portal.screenshot('output/local/portal.png');
// A logout must clear draft file blobs and metadata, including other open Site tabs. // A logout must clear draft file blobs and metadata, including other open Site tabs.
await portal.eval(`(async()=>{const db=await new Promise((resolve,reject)=>{const r=indexedDB.open('dtf-local-cart',1);r.onsuccess=()=>resolve(r.result);r.onerror=reject;});await new Promise((resolve,reject)=>{const tx=db.transaction('cart','readwrite');tx.objectStore('cart').put({items:[new File(['private'],'private.cdr')],expires:Date.now()+86400000},'security-fixture');tx.oncomplete=resolve;tx.onerror=reject;});db.close();})()`); await portal.eval(`(async()=>{const db=await new Promise((resolve,reject)=>{const r=indexedDB.open('dtf-local-cart',1);r.onsuccess=()=>resolve(r.result);r.onerror=reject;});await new Promise((resolve,reject)=>{const tx=db.transaction('cart','readwrite');tx.objectStore('cart').put({items:[new File(['private'],'private.cdr')],expires:Date.now()+86400000},'security-fixture');tx.oncomplete=resolve;tx.onerror=reject;});db.close();})()`);

View File

@@ -15,7 +15,23 @@ def run():
password='local-test-password-'+uuid4().hex password='local-test-password-'+uuid4().hex
customer.call('/account/register',{'customer':profile,'password':password}) customer.call('/account/register',{'customer':profile,'password':password})
assert customer.call('/account/me')['customer']['mail']==profile['mail'] assert customer.call('/account/me')['customer']['mail']==profile['mail']
assert customer.call('/customer/orders')['orders'][0]['id']==oid assert [e['id'] for e in customer.call('/customer/orders')['items'] if e['kind']=='order'][0]==oid
# The list filters, pages and counts by group.
listing=customer.call('/customer/orders?status=prod&page=1&size=1')
assert listing['counts']['prod']>=1 and listing['total']==listing['counts']['prod'] and len(listing['items'])==1, listing
assert customer.call('/customer/orders?status=fin')['items']==[]
assert customer.call('/customer/orders?number='+str(order['number']))['items'][0]['id']==oid
# The account's details change; the CNPJ does not, and e-mail and password need the password.
address={'recipient':'Workflow Ltda','street':'Rua de Teste','number':'10','complement':'',
'district':'Centro','city':'Franca','state':'SP','postal_code':'14400000'}
saved=customer.call('/account/profile',{'zap':'(16) 98888-7777','address':address})['customer']
assert saved['zap']=='16988887777' and saved['cnpj']==profile['cnpj'] and saved['address']['city']=='Franca'
assert customer.call('/account/me')['customer']['address']['postal_code']=='14400000'
customer.call('/account/profile',{'zap':'16988887777','cnpj':'00000000000000'},expected=422)
customer.call('/account/email',{'email':'x-'+profile['mail'],'password':'wrong-password'},expected=401)
customer.call('/account/password',{'current':'wrong-password','new':'another-password-123'},expected=401)
other.call('/account/profile',{'zap':'16988887777'},expected=401)
print('PASS: customer order list filters and pages; account details change, CNPJ does not')
# Email/CNPJ do not grant ownership; only current guest session is migrated. # Email/CNPJ do not grant ownership; only current guest session is migrated.
other.call('/customer/orders/'+oid,expected=404) other.call('/customer/orders/'+oid,expected=404)
other.call('/account/login',{'email':profile['mail'],'password':'wrong-password'},expected=401) other.call('/account/login',{'email':profile['mail'],'password':'wrong-password'},expected=401)
@@ -72,7 +88,7 @@ def run():
old_cookie=list(other.jar)[0].value old_cookie=list(other.jar)[0].value
other.call('/account/logout',{}) other.call('/account/logout',{})
cookie.value=old_cookie;revoked.jar.set_cookie(cookie);revoked.call('/customer/orders',expected=401) cookie.value=old_cookie;revoked.jar.set_cookie(cookie);revoked.call('/customer/orders',expected=401)
other.call('/session');assert other.call('/customer/orders')['orders']==[] other.call('/session');assert other.call('/customer/orders')['items']==[]
print('PASS: logout revokes server session and signed-out visitors cannot see account orders') print('PASS: logout revokes server session and signed-out visitors cannot see account orders')
if __name__=='__main__':run() if __name__=='__main__':run()

View File

@@ -39,9 +39,23 @@
pedido=saved.items; pedido=saved.items;
$('atual').style.display='none';pintaEntrega(); $('atual').style.display='none';pintaEntrega();
notice.textContent='Carrinho recuperado. Remova e adicione novamente um item se precisar alterar sua montagem.'; notice.textContent='Carrinho recuperado. Remova e adicione novamente um item se precisar alterar sua montagem.';
}else{ }
// A signed-in customer's details come from the account, with the saved
// delivery address, whether or not a cart was recovered.
const account=await window.dtfApi('/account/me'); const account=await window.dtfApi('/account/me');
if(account.customer && !cliente.mail){cliente={...account.customer};for(const [id,key] of [['fCnpj','cnpj'],['fZap','zap'],['fMail','mail']])$(id).value=cliente[key];pintaEntrega();} const c=account.customer;
if(c && !cliente.mail){
cliente={cnpj:c.cnpj,zap:c.zap,mail:c.mail};
// The fields' own input handlers format them, as if typed.
for(const [id,key] of [['fCnpj','cnpj'],['fZap','zap'],['fMail','mail']]){$(id).value=cliente[key];$(id).dispatchEvent(new Event('input'));}
const a=c.address;
if(a && !entrega.cep){
entrega.cep=a.postal_code;
entrega.end={nome:a.recipient,rua:a.street,num:a.number,comp:a.complement||'',bairro:a.district,cidade:a.city,uf:a.state};
$('cepIn').value=a.postal_code.replace(/^(\d{5})(\d{3})$/,'$1-$2');
for(const [id,key] of [['eNome','nome'],['eRua','rua'],['eNum','num'],['eComp','comp'],['eBairro','bairro'],['eCidade','cidade'],['eUf','uf']])$(id).value=entrega.end[key];
}
pintaEntrega();
} }
}catch(error){notice.textContent='Recuperação de carrinho indisponível; o pedido ainda pode ser feito nesta sessão.';} }catch(error){notice.textContent='Recuperação de carrinho indisponível; o pedido ainda pode ser feito nesta sessão.';}
finally{restoring=false;} finally{restoring=false;}

View File

@@ -396,7 +396,7 @@
titulo.insertAdjacentHTML('afterbegin', icone('okCirculo')); titulo.insertAdjacentHTML('afterbegin', icone('okCirculo'));
box.append(titulo, box.append(titulo,
node('p','Pedido #'+quote.order.number+' recebido e enviado para a produção. Você acompanha cada etapa em Meus pedidos.')); node('p','Pedido #'+quote.order.number+' recebido e enviado para a produção. Você acompanha cada etapa em Meus pedidos.'));
button('Ver meus pedidos',()=>location.assign('/portal.html?order='+quote.order.id),box); button('Ver meus pedidos',()=>location.assign('/conta/pedidos?order='+quote.order.id),box);
actions.append(box); actions.append(box);
} }
const cartOk=()=>!!quotedCart && quotedCart===cartSnapshot(); const cartOk=()=>!!quotedCart && quotedCart===cartSnapshot();

View File

@@ -651,7 +651,7 @@ h2 em{font-style:normal;color:var(--laranja)}
<header class="topo"><div class="w"> <header class="topo"><div class="w">
<a class="logo" href="https://www.dropstaratacado.com.br" aria-label="Dropstar Atacado"><img src="https://cdn.vnda.com.br/dropstar/2025/12/17/logo-header-15125700-16129080.png?v=1766009286" alt="Dropstar"></a> <a class="logo" href="https://www.dropstaratacado.com.br" aria-label="Dropstar Atacado"><img src="https://cdn.vnda.com.br/dropstar/2025/12/17/logo-header-15125700-16129080.png?v=1766009286" alt="Dropstar"></a>
<div class="acts"> <div class="acts">
<a href="/portal.html"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg><span class="actTx">Minha conta</span></a> <a href="/conta"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg><span class="actTx">Minha conta</span></a>
<a href="/carrinho" id="cartLink" aria-label="Carrinho, 0 itens"><span class="cartIc"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg><b class="cartN" id="cartN" hidden>0</b></span><span class="actTx">Carrinho</span></a><span class="cartEnvio" id="cartEnvio" aria-live="polite"></span> <a href="/carrinho" id="cartLink" aria-label="Carrinho, 0 itens"><span class="cartIc"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg><b class="cartN" id="cartN" hidden>0</b></span><span class="actTx">Carrinho</span></a><span class="cartEnvio" id="cartEnvio" aria-live="polite"></span>
</div> </div>
</div></header> </div></header>

View File

@@ -1,67 +1,99 @@
/* Meus pedidos: the Site's look (tokens and shell from site-shell.css and /* Minha conta: the Site's look (tokens and shell from site-shell.css and
site-v2.css) for the order page. */ site-v2.css) for sign-in, the overview, the orders and the details. */
[hidden]{display:none!important} [hidden]{display:none!important}
body{background:var(--cinza)} body{background:var(--cinza)}
.topo .acts a[aria-current="page"]{color:var(--laranja-escuro)} .topo .acts a[aria-current="page"]{color:var(--laranja-escuro)}
.ped{padding-top:26px;padding-bottom:64px;min-height:70vh} #quem{max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.volta{display:inline-block;margin-bottom:14px;font-size:15px;color:var(--texto2);text-decoration:none} .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.volta:hover{color:var(--laranja-escuro)} .ped{padding-top:28px;padding-bottom:64px;min-height:70vh}
.pedCab{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:18px} .ped h1{font-size:30px;font-weight:800;margin-bottom:14px}
.pedCab h1{font-size:34px;font-weight:800;margin-top:4px}
.conta{margin-top:6px;font-size:15px;color:var(--fraco)}
.conta:empty{display:none}
.pedAcoes{display:flex;gap:10px}
.pedAviso{margin-bottom:14px;padding:12px 16px;border-radius:var(--raio2);background:var(--aviso-fundo);border:1px solid var(--aviso-linha);font-size:15px} .pedAviso{margin-bottom:14px;padding:12px 16px;border-radius:var(--raio2);background:var(--aviso-fundo);border:1px solid var(--aviso-linha);font-size:15px}
.pedMsg{margin-bottom:14px;font-size:15px;color:var(--laranja-escuro)} .pedMsg{margin-bottom:14px;padding:10px 14px;border-radius:var(--raio2);background:var(--verde-fundo);color:var(--verde);font-size:15px}
.pedMsg.erro{background:#FDE8E6;color:#B42318}
.pedMsg:empty{display:none} .pedMsg:empty{display:none}
/* Buttons, as on the Site. */ /* Buttons, as on the Site. */
.bPri{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:48px;padding:0 22px;border:0;border-radius:var(--raio2);background:var(--laranja);color:var(--preto);font-family:"Inter",sans-serif;font-weight:800;font-size:16px;text-decoration:none;cursor:pointer} .bPri{display:inline-flex;align-items:center;justify-content:center;gap:8px;height:48px;padding:0 22px;border:0;border-radius:var(--raio2);background:var(--laranja);color:var(--preto);font-family:"Inter",sans-serif;font-weight:800;font-size:16px;text-decoration:none;cursor:pointer}
.bPri:hover:not(:disabled){background:#FFB53D} .bPri:hover:not(:disabled){background:#FFB53D}
.bPri.bMini{height:38px;padding:0 16px;font-size:15px}
.bSec{display:inline-flex;align-items:center;justify-content:center;gap:7px;height:40px;padding:0 16px;border:1px solid #D6D6D6;border-radius:var(--raio2);background:#fff;color:var(--preto);font-family:"Inter",sans-serif;font-weight:700;font-size:15px;cursor:pointer} .bSec{display:inline-flex;align-items:center;justify-content:center;gap:7px;height:40px;padding:0 16px;border:1px solid #D6D6D6;border-radius:var(--raio2);background:#fff;color:var(--preto);font-family:"Inter",sans-serif;font-weight:700;font-size:15px;cursor:pointer}
.bSec:hover:not(:disabled){border-color:var(--preto)} .bSec:hover:not(:disabled){border-color:var(--preto)}
.bSec .ic{width:18px;height:18px} .bSec .ic{width:18px;height:18px}
.bLink{margin-top:14px;padding:0;border:0;background:none;color:var(--laranja-escuro);font-family:"Inter",sans-serif;font-weight:700;font-size:15px;cursor:pointer;text-decoration:underline;text-underline-offset:3px} .bLink{align-self:flex-start;margin-top:12px;padding:0;border:0;background:none;color:var(--laranja-escuro);font-family:inherit;font-weight:600;font-size:15px;cursor:pointer;text-decoration:underline;text-underline-offset:3px}
button:disabled{opacity:.6;cursor:default} button:disabled{opacity:.6;cursor:default}
/* Sign in / create account. */ /* Forms. */
.acesso{margin-bottom:26px} .acessoGrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:16px;align-items:start}
.acessoIntro{margin-bottom:12px;font-size:15px;color:var(--texto2)} .bloco{display:flex;flex-direction:column;margin-bottom:16px;padding:22px;border:1px solid var(--linha);border-radius:var(--raio);background:#fff}
.acessoIntro:empty{display:none} .bloco h2{font-size:20px;font-weight:800}
.acessoGrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:16px} .nota2{margin:4px 0 8px;font-size:14.5px;color:var(--fraco)}
.bloco{display:flex;flex-direction:column;padding:22px;border:1px solid var(--linha);border-radius:var(--raio);background:#fff} .nota2 a{color:var(--laranja-escuro)}
.bloco h2{font-size:21px;font-weight:800} .bloco label{margin:10px 0 5px;font-size:14px;font-weight:600;color:var(--texto2)}
.bloco .nota2{margin:4px 0 12px;font-size:14.5px;color:var(--fraco)}
.bloco label,.correcaoForm label{margin:10px 0 5px;font-size:14px;font-weight:600;color:var(--texto2)}
.bloco label .obs{font-weight:400;color:var(--fraco)} .bloco label .obs{font-weight:400;color:var(--fraco)}
.bloco input,.correcaoForm textarea{width:100%;height:46px;padding:0 13px;border:1px solid #D6D6D6;border-radius:var(--raio2);font:inherit;font-size:16px;background:#fff} .bloco input,.busca2 input,.busca2 select,.correcaoForm textarea{width:100%;height:46px;padding:0 13px;border:1px solid #D6D6D6;border-radius:var(--raio2);font:inherit;font-size:16px;background:#fff;color:var(--preto)}
.correcaoForm textarea{height:auto;padding:10px 13px;resize:vertical} .bloco input[readonly]{background:var(--cinza);color:var(--texto2)}
.bloco input:focus,.correcaoForm textarea:focus{outline:none;border-color:var(--laranja);box-shadow:0 0 0 3px rgba(255,168,26,.2)} .bloco input:focus,.busca2 input:focus,.busca2 select:focus,.correcaoForm textarea:focus{outline:none;border-color:var(--laranja);box-shadow:0 0 0 3px rgba(255,168,26,.2)}
.bloco .bPri{margin-top:16px} .bloco>.bPri,.bloco>.bSec{margin-top:16px;align-self:flex-start}
.bloco .nota2:last-child{margin:12px 0 0}
.duas{display:grid;grid-template-columns:1fr 1fr;gap:0 12px} .duas{display:grid;grid-template-columns:1fr 1fr;gap:0 12px}
.duas>div{display:flex;flex-direction:column} .tres{display:grid;grid-template-columns:1fr 1fr 90px;gap:0 12px}
.duas>div,.tres>div{display:flex;flex-direction:column}
/* Orders. */ /* The account area: menu on the side, one view beside it. */
.grupo{margin-bottom:22px} .area{display:grid;grid-template-columns:210px minmax(0,1fr);gap:28px;align-items:start}
.grupo:empty{display:none} .lateral{position:sticky;top:90px;display:flex;flex-direction:column;gap:2px;padding:10px;border:1px solid var(--linha);border-radius:var(--raio);background:#fff}
.tituloGrupo{margin-bottom:12px;font-size:20px;font-weight:800} .lateral a,.lateral button{display:block;padding:10px 12px;border:0;border-radius:var(--raio2);background:none;color:var(--texto2);font:inherit;font-size:15.5px;text-align:left;text-decoration:none;cursor:pointer}
.pedido{margin-bottom:14px;padding:20px 22px;border:1px solid var(--linha);border-radius:var(--raio);background:#fff} .lateral a:hover,.lateral button:hover{background:var(--cinza);color:var(--preto)}
.pedido.pendente{border-color:var(--aviso-linha)} .lateral a.on{background:var(--aviso-fundo);color:var(--laranja-escuro);font-weight:700}
.pedido.correcao{border-color:#F3B7A9} .lateral button{margin-top:6px;border-top:1px solid var(--linha);border-radius:0 0 var(--raio2) var(--raio2)}
.pedTopo{display:flex;align-items:center;gap:14px;flex-wrap:wrap} .conta{margin:-8px 0 16px;font-size:15px;color:var(--fraco)}
.pedTopo>div{flex:1;min-width:160px} .acessoIntro{margin:-6px 0 14px;font-size:15px;color:var(--texto2)}
.pedTopo h3{font-size:19px;font-weight:800} .acessoIntro:empty{display:none}
.pedTopo .data{font-size:14.5px;color:var(--fraco)} .tituloGrupo{margin:22px 0 10px;font-size:18px;font-weight:800}
.pedTopo .valor{font-family:"Inter",sans-serif;font-size:19px;font-weight:800}
.pedido.pendente .bPri{margin-top:14px} /* Overview. */
.pedido .nota2{margin-top:10px;font-size:15px;color:var(--fraco)} .metricas{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px}
.estado{padding:5px 11px;border-radius:999px;font-family:"Inter",sans-serif;font-size:14px;font-weight:700;background:var(--cinza);color:var(--texto2);white-space:nowrap} .metrica{display:flex;flex-direction:column;gap:4px;padding:16px 18px;border:1px solid var(--linha);border-radius:var(--raio);background:#fff;color:var(--texto2);font-size:14.5px;text-decoration:none}
.metrica:hover{border-color:var(--laranja)}
.metrica b{font-family:"Inter",sans-serif;font-size:28px;font-weight:800;color:var(--preto)}
.metrica.alerta{border-color:#F3B7A9;background:#FFF6F4}
.verTodos{display:inline-block;margin-top:12px;color:var(--laranja-escuro);font-weight:700;text-decoration:none}
/* Orders: filters, rows, pages. */
.filtros{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px}
.chipF{height:36px;padding:0 14px;border:1px solid #D6D6D6;border-radius:999px;background:#fff;color:var(--texto2);font:inherit;font-size:14.5px;font-weight:600;cursor:pointer}
.chipF:hover{border-color:var(--preto)}
.chipF.on{border-color:var(--laranja);background:var(--aviso-fundo);color:var(--laranja-escuro)}
.busca2{display:grid;grid-template-columns:minmax(0,1fr) 200px;gap:10px;margin-bottom:12px}
.lista{border:1px solid var(--linha);border-radius:var(--raio);background:#fff;overflow:hidden}
.lista:empty{display:none}
.item+.item{border-top:1px solid var(--linha)}
.item.pendente{background:#FFFCF6}
.linha{display:grid;grid-template-columns:90px minmax(0,1fr) auto 110px 90px;gap:14px;align-items:center;padding:14px 18px;cursor:pointer}
.item.pendente .linha{cursor:default}
.linha:hover{background:var(--cinza)}
.linha .num{font-family:"Inter",sans-serif;font-size:16px}
.oque{display:flex;flex-direction:column;min-width:0}
.oque span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:15px}
.oque .data{font-size:14px;color:var(--fraco)}
.estadoW{display:flex;align-items:center;gap:6px}
.estado{padding:4px 10px;border-radius:999px;font-family:"Inter",sans-serif;font-size:13.5px;font-weight:700;background:var(--cinza);color:var(--texto2);white-space:nowrap}
.via{font-size:13px;font-weight:700;color:var(--fraco)}
.est-fin{background:var(--verde-fundo);color:var(--verde)} .est-fin{background:var(--verde-fundo);color:var(--verde)}
.est-cor{background:#FDE8E6;color:#B42318} .est-cor{background:#FDE8E6;color:#B42318}
.est-imp,.est-fil,.est-tra,.est-rec{background:var(--aviso-fundo);color:var(--laranja-escuro)} .est-pay{background:#FFF1D6;color:#8A4B00}
.est-rev{background:#EEF1F5;color:#45505E}
.est-imp,.est-fil,.est-tra,.est-rec{background:#E7F0FB;color:#1D4E89}
.linha .valor{font-family:"Inter",sans-serif;font-size:16px;text-align:right}
.acao{display:flex;justify-content:flex-end}
.acao .bSec{height:36px;padding:0 14px}
.paginas{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:12px}
.pags{display:flex;gap:6px}
.pag{min-width:38px;height:38px;border:1px solid #D6D6D6;border-radius:var(--raio2);background:#fff;font:inherit;font-weight:700;cursor:pointer}
.pag.on{border-color:var(--laranja);background:var(--aviso-fundo);color:var(--laranja-escuro);cursor:default}
.vazio{display:flex;flex-direction:column;align-items:flex-start;gap:12px;padding:26px 22px;font-size:16px;color:var(--texto2)}
/* Progress through production. */ /* An order opened in the list. */
.detalhe{padding:4px 18px 18px;border-top:1px dashed var(--linha);background:#fff}
.trilha{display:flex;list-style:none;margin:18px 0 6px;padding:0} .trilha{display:flex;list-style:none;margin:18px 0 6px;padding:0}
.trilha li{flex:1;position:relative;display:flex;flex-direction:column;align-items:center;gap:6px;text-align:center;font-size:14px;color:var(--fraco)} .trilha li{flex:1;position:relative;display:flex;flex-direction:column;align-items:center;gap:6px;text-align:center;font-size:14px;color:var(--fraco)}
.trilha li::before{content:"";position:absolute;top:14px;left:-50%;width:100%;height:2px;background:#E0E0E0;z-index:0} .trilha li::before{content:"";position:absolute;top:14px;left:-50%;width:100%;height:2px;background:#E0E0E0;z-index:0}
@@ -77,7 +109,6 @@ button:disabled{opacity:.6;cursor:default}
.trilha li.atual b{font-weight:800} .trilha li.atual b{font-weight:800}
.pedAlerta{display:flex;gap:10px;align-items:flex-start;margin:16px 0 4px;padding:12px 14px;border-radius:var(--raio2);background:#FDE8E6;color:#7A1C10;font-size:15px} .pedAlerta{display:flex;gap:10px;align-items:flex-start;margin:16px 0 4px;padding:12px 14px;border-radius:var(--raio2);background:#FDE8E6;color:#7A1C10;font-size:15px}
.pedAlerta .ic{flex:none;width:20px;height:20px;margin-top:1px;color:#B42318} .pedAlerta .ic{flex:none;width:20px;height:20px;margin-top:1px;color:#B42318}
.itensPed{list-style:none;margin:14px 0 0;padding:0;border-top:1px solid var(--linha)} .itensPed{list-style:none;margin:14px 0 0;padding:0;border-top:1px solid var(--linha)}
.itensPed li{display:flex;gap:12px;align-items:baseline;padding:10px 0;border-bottom:1px solid var(--linha);font-size:15.5px} .itensPed li{display:flex;gap:12px;align-items:baseline;padding:10px 0;border-bottom:1px solid var(--linha);font-size:15.5px}
.itensPed .nome{flex:1;font-weight:600} .itensPed .nome{flex:1;font-weight:600}
@@ -85,12 +116,8 @@ button:disabled{opacity:.6;cursor:default}
.itensPed b{min-width:96px;text-align:right;font-family:"Inter",sans-serif} .itensPed b{min-width:96px;text-align:right;font-family:"Inter",sans-serif}
.itensPed .frete .nome{font-weight:400;color:var(--texto2)} .itensPed .frete .nome{font-weight:400;color:var(--texto2)}
.entregaPed{display:flex;align-items:center;gap:8px;margin-top:12px;font-size:15px;color:var(--texto2)} .entregaPed{display:flex;align-items:center;gap:8px;margin-top:12px;font-size:15px;color:var(--texto2)}
.entregaPed .ic{width:19px;height:19px;color:var(--laranja-escuro)} .entregaPed .ic{flex:none;width:19px;height:19px;color:var(--laranja-escuro)}
.secao{margin-top:16px}
/* Details. */
.detalhe:empty{display:none}
.detalhe{margin-top:14px;padding-top:4px;border-top:1px dashed var(--linha)}
.secao{margin-top:14px}
.secao h4{margin-bottom:8px;font-size:16px;font-weight:800} .secao h4{margin-bottom:8px;font-size:16px;font-weight:800}
.historico{list-style:none;margin:0;padding:0 0 0 14px;border-left:2px solid var(--linha)} .historico{list-style:none;margin:0;padding:0 0 0 14px;border-left:2px solid var(--linha)}
.historico li{position:relative;padding:0 0 12px 12px;font-size:15px} .historico li{position:relative;padding:0 0 12px 12px;font-size:15px}
@@ -103,14 +130,23 @@ button:disabled{opacity:.6;cursor:default}
.arquivo b{overflow-wrap:anywhere;font-size:15px} .arquivo b{overflow-wrap:anywhere;font-size:15px}
.arquivo span{font-size:14px;color:var(--fraco)} .arquivo span{font-size:14px;color:var(--fraco)}
.correcaoForm{display:flex;flex-direction:column;padding:16px;border-radius:var(--raio2);background:var(--cinza)} .correcaoForm{display:flex;flex-direction:column;padding:16px;border-radius:var(--raio2);background:var(--cinza)}
.correcaoForm input[type=file]{font-size:15px} .correcaoForm label{margin:10px 0 5px;font-size:14px;font-weight:600;color:var(--texto2)}
.correcaoForm textarea{height:auto;padding:10px 13px;resize:vertical}
.correcaoForm .bPri{margin-top:14px;align-self:flex-start} .correcaoForm .bPri{margin-top:14px;align-self:flex-start}
.vazio{display:flex;flex-direction:column;align-items:flex-start;gap:12px;padding:26px 22px;border:1px dashed #D6D6D6;border-radius:var(--raio);background:#fff;font-size:16px;color:var(--texto2)} @media (max-width:820px){
.area{grid-template-columns:1fr;gap:16px}
@media (max-width:600px){ .lateral{position:static;flex-direction:row;flex-wrap:wrap}
.pedCab h1{font-size:28px} .lateral button{margin:0;border:0;border-radius:var(--raio2)}
.trilha b{font-size:12.5px} .linha{grid-template-columns:minmax(0,1fr) auto;gap:6px 12px}
.duas{grid-template-columns:1fr} .linha .num{grid-column:1}
.pedido{padding:16px} .linha .oque{grid-column:1/-1;grid-row:2}
.linha .estadoW{grid-column:2;grid-row:1;justify-self:end}
.linha .valor{grid-column:1;grid-row:3;text-align:left}
.linha .acao{grid-column:2;grid-row:3}
}
@media (max-width:600px){
.ped h1{font-size:26px}
.trilha b{font-size:12.5px}
.duas,.tres,.busca2{grid-template-columns:1fr}
} }

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>Meus pedidos · DTF Dropstar</title> <title>Minha conta · DTF Dropstar</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
@@ -20,38 +20,29 @@
<header class="topo"><div class="w"> <header class="topo"><div class="w">
<a class="logo" href="https://www.dropstaratacado.com.br" aria-label="Dropstar Atacado"><img src="https://cdn.vnda.com.br/dropstar/2025/12/17/logo-header-15125700-16129080.png?v=1766009286" alt="Dropstar"></a> <a class="logo" href="https://www.dropstaratacado.com.br" aria-label="Dropstar Atacado"><img src="https://cdn.vnda.com.br/dropstar/2025/12/17/logo-header-15125700-16129080.png?v=1766009286" alt="Dropstar"></a>
<div class="acts"> <div class="acts">
<a href="/portal.html" aria-current="page"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg><span class="actTx">Minha conta</span></a> <a href="/conta" aria-current="page"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg><span class="actTx" id="quem">Minha conta</span></a>
<a href="/carrinho"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg><span class="actTx">Carrinho</span></a> <a href="/carrinho"><svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg><span class="actTx">Carrinho</span></a>
</div> </div>
</div></header> </div></header>
<main class="ped w"> <main class="ped w">
<a class="volta" href="/#envio">‹ Enviar outra arte</a>
<div class="pedCab">
<div>
<p class="et">DTF Dropstar</p>
<h1>Meus pedidos</h1>
<p class="conta" id="account"></p>
</div>
<div class="pedAcoes">
<button type="button" class="bSec" id="refresh">Atualizar</button>
<button type="button" class="bSec" id="logout" hidden>Sair</button>
</div>
</div>
<p class="pedAviso" id="dev-notice" hidden>Ambiente de desenvolvimento local. Pagamentos e notificações são simulados. Use apenas dados de teste.</p> <p class="pedAviso" id="dev-notice" hidden>Ambiente de desenvolvimento local. Pagamentos e notificações são simulados. Use apenas dados de teste.</p>
<p class="pedMsg" id="message" role="status"></p> <p class="pedMsg" id="message" role="status"></p>
<section class="acesso" id="auth" aria-label="Entrar ou criar conta"> <!-- /conta/entrar -->
<p class="acessoIntro" id="guest"></p> <section class="acesso" id="auth" hidden aria-labelledby="tEntrar">
<h1 id="tEntrar">Minha conta</h1>
<div class="acessoGrid"> <div class="acessoGrid">
<form class="bloco" id="login"> <form class="bloco" id="login">
<h2>Entrar</h2> <h2>Entrar</h2>
<p class="nota2">Já comprou com seu CNPJ? Entre para ver todos os seus pedidos.</p> <p class="nota2">Para ver seus pedidos e seus dados.</p>
<label for="email">E-mail</label> <label for="email">E-mail</label>
<input id="email" type="email" autocomplete="username" required> <input id="email" type="email" autocomplete="username" required>
<label for="password">Senha</label> <label for="password">Senha</label>
<input id="password" type="password" autocomplete="current-password" required> <input id="password" type="password" autocomplete="current-password" required>
<button class="bPri">Entrar</button> <button class="bPri">Entrar</button>
<button type="button" class="bLink" id="esqueci">Esqueci minha senha</button>
<p class="nota2" id="esqueciTx" hidden>A recuperação por e-mail chega em breve. Enquanto isso, fale com a Dropstar pelo WhatsApp <a href="https://wa.me/message/NJ76CLIOUGXLB1" target="_blank" rel="noopener">16 99428-0056</a>.</p>
</form> </form>
<form class="bloco" id="register"> <form class="bloco" id="register">
<h2>Criar conta</h2> <h2>Criar conta</h2>
@@ -65,13 +56,89 @@
<label for="register-password">Senha <span class="obs">12 caracteres ou mais</span></label> <label for="register-password">Senha <span class="obs">12 caracteres ou mais</span></label>
<input id="register-password" type="password" minlength="12" maxlength="128" autocomplete="new-password" required> <input id="register-password" type="password" minlength="12" maxlength="128" autocomplete="new-password" required>
<button class="bPri">Criar conta</button> <button class="bPri">Criar conta</button>
<p class="nota2">Guarde sua senha: a recuperação por e-mail ainda não está disponível.</p>
</form> </form>
</div> </div>
</section> </section>
<section class="grupo" id="quotes" aria-label="Aguardando pagamento"></section> <!-- /conta, /conta/pedidos, /conta/dados -->
<section class="grupo" id="orders" aria-label="Pedidos"></section> <div class="area" id="area" hidden>
<nav class="lateral" aria-label="Minha conta">
<a href="/conta" data-view="geral" data-conta>Visão geral</a>
<a href="/conta/pedidos" data-view="pedidos">Pedidos</a>
<a href="/conta/dados" data-view="dados" data-conta>Dados da conta</a>
<a href="/conta/entrar" data-view="entrar" id="navEntrar">Entrar</a>
<button type="button" id="logout" hidden>Sair</button>
</nav>
<section class="vista" id="vGeral" hidden aria-labelledby="tGeral">
<h1 id="tGeral">Olá</h1>
<p class="conta" id="account"></p>
<div class="metricas" id="metricas"></div>
<h2 class="tituloGrupo">Último pedido</h2>
<div class="lista" id="ultimo"></div>
<a class="verTodos" href="/conta/pedidos" data-view="pedidos">Ver todos os pedidos ›</a>
</section>
<section class="vista" id="vPedidos" hidden aria-labelledby="tPedidos">
<h1 id="tPedidos">Pedidos</h1>
<p class="acessoIntro" id="guest"></p>
<div class="filtros" id="filtros" role="group" aria-label="Filtrar pedidos"></div>
<div class="busca2">
<label class="sr" for="fNumero">Número do pedido</label>
<input id="fNumero" inputmode="numeric" placeholder="Nº do pedido">
<label class="sr" for="fPeriodo">Período</label>
<select id="fPeriodo">
<option value="30">Últimos 30 dias</option>
<option value="90">Últimos 90 dias</option>
<option value="365">Último ano</option>
<option value="0" selected>Todo o período</option>
</select>
</div>
<div class="lista" id="orders"></div>
<div class="paginas" id="paginas"></div>
</section>
<section class="vista" id="vDados" hidden aria-labelledby="tDados">
<h1 id="tDados">Dados da conta</h1>
<form class="bloco" id="fPerfil">
<h2>Empresa e entrega</h2>
<div class="duas">
<div><label for="dCnpj">CNPJ</label><input id="dCnpj" readonly aria-readonly="true"></div>
<div><label for="dZap">WhatsApp</label><input id="dZap" inputmode="numeric" autocomplete="tel" required></div>
</div>
<p class="nota2">Endereço de entrega: preenche o carrinho quando você escolhe receber em casa. Deixe em branco para não guardar.</p>
<div class="duas">
<div><label for="dCep">CEP</label><input id="dCep" inputmode="numeric" maxlength="9" placeholder="00000-000"></div>
<div><label for="dNome">Quem recebe</label><input id="dNome" maxlength="120"></div>
</div>
<label for="dRua">Rua / avenida</label><input id="dRua" maxlength="160">
<div class="duas">
<div><label for="dNum">Número</label><input id="dNum" maxlength="20"></div>
<div><label for="dComp">Complemento (opcional)</label><input id="dComp" maxlength="80"></div>
</div>
<div class="tres">
<div><label for="dBairro">Bairro</label><input id="dBairro" maxlength="80"></div>
<div><label for="dCidade">Cidade</label><input id="dCidade" maxlength="80"></div>
<div><label for="dUf">UF</label><input id="dUf" maxlength="2"></div>
</div>
<button class="bPri">Salvar</button>
</form>
<form class="bloco" id="fEmail">
<h2>E-mail</h2>
<p class="nota2">É o seu acesso e para onde vai a nota fiscal.</p>
<label for="dMail">E-mail</label><input id="dMail" type="email" autocomplete="email" required>
<label for="dMailSenha">Senha atual, para confirmar</label><input id="dMailSenha" type="password" autocomplete="current-password" required>
<button class="bSec">Alterar e-mail</button>
</form>
<form class="bloco" id="fSenha">
<h2>Senha</h2>
<label for="dAtual">Senha atual</label><input id="dAtual" type="password" autocomplete="current-password" required>
<label for="dNova">Nova senha <span class="obs">12 caracteres ou mais</span></label><input id="dNova" type="password" minlength="12" maxlength="128" autocomplete="new-password" required>
<button class="bSec">Alterar senha</button>
<p class="nota2">Os outros aparelhos conectados saem da conta.</p>
</form>
</section>
</div>
</main> </main>
<footer><div class="w"> <footer><div class="w">

View File

@@ -1,11 +1,17 @@
/* Meus pedidos: the customer's orders, their progress and files, sign-in and /* Minha conta: sign-in, the overview, the orders (filtered, a page at a time,
the artwork correction the team may ask for. */ with the cart waiting for payment among them) and the account's details.
One page; the address says which part shows (/conta, /conta/entrar,
/conta/pedidos, /conta/dados). */
const $=id=>document.getElementById(id); const $=id=>document.getElementById(id);
const node=(tag,text,className)=>{const el=document.createElement(tag);if(text!==undefined&&text!==null)el.textContent=text;if(className)el.className=className;return el;}; const node=(tag,text,className)=>{const el=document.createElement(tag);if(text!==undefined&&text!==null)el.textContent=text;if(className)el.className=className;return el;};
const money=c=>(c/100).toLocaleString('pt-BR',{style:'currency',currency:'BRL'}); const money=c=>(c/100).toLocaleString('pt-BR',{style:'currency',currency:'BRL'});
const quando=d=>new Date(d).toLocaleString('pt-BR',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}); const quando=d=>new Date(d).toLocaleString('pt-BR',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
const dia=d=>new Date(d).toLocaleDateString('pt-BR'); const dia=d=>new Date(d).toLocaleDateString('pt-BR',{day:'2-digit',month:'2-digit',year:'2-digit'});
const metros=m=>Number(m).toLocaleString('pt-BR',{minimumFractionDigits:2,maximumFractionDigits:2})+' m'; const metros=m=>Number(m).toLocaleString('pt-BR',{minimumFractionDigits:2,maximumFractionDigits:2})+' m';
const soDigitos=v=>String(v||'').replace(/\D/g,'');
const fmtCnpj=v=>soDigitos(v).replace(/^(\d{2})(\d{3})(\d{3})(\d{4})(\d{2})$/,'$1.$2.$3/$4-$5');
const fmtZap=v=>{const d=soDigitos(v);return d.length===11?'('+d.slice(0,2)+') '+d.slice(2,7)+'-'+d.slice(7):d.length===10?'('+d.slice(0,2)+') '+d.slice(2,6)+'-'+d.slice(6):v;};
const fmtCep=v=>{const d=soDigitos(v).slice(0,8);return d.length>5?d.slice(0,5)+'-'+d.slice(5):d;};
// Lucide shapes (ISC licence), the same as on the Site. // Lucide shapes (ISC licence), the same as on the Site.
const ICONES={ const ICONES={
baixar:'<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/>', baixar:'<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/>',
@@ -13,42 +19,175 @@ const ICONES={
caminhao:'<path d="M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"/><path d="M15 18H9"/><path d="M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.62l-3.48-4.35A1 1 0 0 0 17.52 8H14"/><circle cx="17" cy="18" r="2"/><circle cx="7" cy="18" r="2"/>', caminhao:'<path d="M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2"/><path d="M15 18H9"/><path d="M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.62l-3.48-4.35A1 1 0 0 0 17.52 8H14"/><circle cx="17" cy="18" r="2"/><circle cx="7" cy="18" r="2"/>',
ok:'<path d="M20 6 9 17l-5-5"/>', ok:'<path d="M20 6 9 17l-5-5"/>',
alerta:'<path d="M12 7v6"/><path d="M12 17h.01"/>', alerta:'<path d="M12 7v6"/><path d="M12 17h.01"/>',
relogio:'<circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/>',
}; };
function icone(nome){ function icone(nome){
const span=document.createElement('span');span.className='icw'; const span=document.createElement('span');
span.innerHTML='<svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false">'+ICONES[nome]+'</svg>'; span.innerHTML='<svg class="ic" viewBox="0 0 24 24" aria-hidden="true" focusable="false">'+ICONES[nome]+'</svg>';
return span.firstChild; return span.firstChild;
} }
const PRODUTO={file:'DTF Têxtil · folha montada',avulsa:'DTF Têxtil · artes avulsas', const PRODUTO={file:'DTF Têxtil · folha montada',avulsa:'DTF Têxtil · artes avulsas',
uvfile:'DTF UV · folha montada',uv:'DTF UV · artes avulsas'}; uvfile:'DTF UV · folha montada',uv:'DTF UV · artes avulsas'};
// The order's way through production, as the customer sees it.
const TRILHA=['rec','tra','fil','imp','fin']; const TRILHA=['rec','tra','fil','imp','fin'];
let scope,states={},environment=''; const TAMANHO=10;
let scope,states={},environment='',conta=null;
let filtro={status:'all',number:'',days:0,page:1};
async function api(path,body){const r=await fetch('/api'+path,{headers:{'Content-Type':'application/json'},...(body===undefined?{}:{method:'POST',body:JSON.stringify(body)})});const d=await r.json();if(!r.ok){const error=new Error(typeof d.detail==='string'?d.detail:'Confira os campos informados.');error.status=r.status;throw error;}return d;} async function api(path,body){const r=await fetch('/api'+path,{headers:{'Content-Type':'application/json'},...(body===undefined?{}:{method:'POST',body:JSON.stringify(body)})});const d=await r.json();if(!r.ok){const error=new Error(typeof d.detail==='string'?d.detail:'Confira os campos informados.');error.status=r.status;throw error;}return d;}
const aviso=text=>{$('message').textContent=text||'';}; const aviso=(text,erro)=>{$('message').textContent=text||'';$('message').classList.toggle('erro',!!erro);};
function button(text,fn,className='bSec'){const b=node('button',text,className);b.type='button';b.onclick=async()=>{b.disabled=true;try{await fn();}catch(e){aviso(e.message);}finally{b.disabled=false;}};return b;} function button(text,fn,className='bSec'){const b=node('button',text,className);b.type='button';b.onclick=async event=>{event?.stopPropagation?.();b.disabled=true;try{await fn();}catch(e){aviso(e.message,true);}finally{b.disabled=false;}};return b;}
function forgetCheckout(){for(const k of ['dtf-quote','dtf-request-key','dtf-request-body'])localStorage.removeItem(k);} function forgetCheckout(){for(const k of ['dtf-quote','dtf-request-key','dtf-request-body'])localStorage.removeItem(k);}
// ---- Addresses -----------------------------------------------------------
function vista(){
const p=location.pathname.replace(/\/+$/,'');
if(p==='/conta/entrar') return 'entrar';
if(p==='/conta/dados') return 'dados';
if(p==='/conta') return 'geral';
return 'pedidos'; // /conta/pedidos, and the old /portal.html
}
function ir(caminho){history.pushState({},'',caminho);mostra().catch(e=>aviso(e.message,true));}
window.addEventListener('popstate',()=>mostra().catch(e=>aviso(e.message,true)));
document.addEventListener('click',event=>{
const a=event.target.closest('a[data-view]');
if(!a||event.metaKey||event.ctrlKey||event.shiftKey)return;
event.preventDefault();ir(a.getAttribute('href'));
});
async function mostra(){
aviso('');
const v=vista(), proximo=new URLSearchParams(location.search).get('next');
// Overview and details are the account's; orders also work for a guest.
if((v==='geral'||v==='dados')&&!conta){history.replaceState({},'','/conta/entrar?next='+encodeURIComponent(location.pathname));return mostra();}
if(v==='entrar'&&conta){history.replaceState({},'',proximo&&/^\/conta(\/[a-z]+)?$/.test(proximo)?proximo:'/conta');return mostra();}
$('auth').hidden=v!=='entrar';
$('area').hidden=v==='entrar';
$('vGeral').hidden=v!=='geral';$('vPedidos').hidden=v!=='pedidos';$('vDados').hidden=v!=='dados';
for(const a of document.querySelectorAll('.lateral a'))a.classList.toggle('on',a.dataset.view===v);
for(const a of document.querySelectorAll('.lateral [data-conta]'))a.hidden=!conta;
$('navEntrar').hidden=!!conta;$('logout').hidden=!conta;
document.title=({entrar:'Entrar',geral:'Minha conta',pedidos:'Pedidos',dados:'Dados da conta'})[v]+' · DTF Dropstar';
if(v==='geral')await geral();
else if(v==='pedidos')await pedidos();
else if(v==='dados')dados();
}
// ---- Orders --------------------------------------------------------------
function trilha(state){ function trilha(state){
const ol=node('ol',null,'trilha'); const ol=node('ol',null,'trilha');
const atual=state==='cor'?-1:TRILHA.indexOf(state); const atual=TRILHA.indexOf(state);
TRILHA.forEach((s,i)=>{ TRILHA.forEach((s,i)=>{
const li=node('li',null,i<atual||state==='fin'?'feito':i===atual?'atual':''); const feito=i<atual||state==='fin';
const li=node('li',null,feito?'feito':i===atual?'atual':'');
const marca=node('span',null,'marca'); const marca=node('span',null,'marca');
if(i<atual||state==='fin')marca.append(icone('ok'));else marca.textContent=String(i+1); if(feito)marca.append(icone('ok'));else marca.textContent=String(i+1);
li.append(marca,node('b',states[s]||s)); li.append(marca,node('b',states[s]||s));ol.append(li);
ol.append(li);
}); });
return ol; return ol;
} }
function entrega(snapshot){ function estado(e){
const d=snapshot.destination, p=node('p',null,'entregaPed'); if(e.kind==='quote')return e.state==='review'?['Em conferência','est-rev']:['Aguardando pagamento','est-pay'];
p.append(icone(d?'caminhao':'local'),node('span',d?'Entrega pela Jadlog · '+d.city+'/'+d.state:'Retirada em Franca · Av. Major Elias Motta, 1281')); return [states[e.state]||e.state,'est-'+e.state];
return p;
} }
function itens(snapshot){ function resumoItens(items){
if(!items.length)return '';
const total=items.reduce((t,i)=>t+Number(i.billed_metres||0),0);
return (items.length===1?PRODUTO[items[0].mode]||items[0].mode:items.length+' itens')+(total?' · '+metros(total):'');
}
function linha(e){
const row=node('div',null,'linha');
const [rotulo,cls]=estado(e);
const num=node('b',e.kind==='quote'?'Carrinho':'#'+e.number,'num');
const oque=node('div',null,'oque');oque.append(node('span',resumoItens(e.items)),node('span',dia(e.created_at),'data'));
const st=node('span',null,'estadoW');st.append(node('span',rotulo,'estado '+cls));
if(e.kind==='quote'&&e.pay_with==='pix')st.append(node('span','PIX','via'));
const valor=node('b',e.total_cents!=null?money(e.total_cents):'—','valor');
const acao=node('div',null,'acao');
row.append(num,oque,st,valor,acao);
const wrap=node('div',null,'item'+(e.kind==='quote'?' pendente':'')+(e.state==='cor'?' correcao':''));
wrap.append(row);
if(e.kind==='quote'){
if(e.state==='pay'){
const pagar=node('a','Pagar','bPri bMini');
pagar.href=(e.pay_with==='pix'?'/pagamento/pix':'/pagamento')+'?quote='+e.id;
acao.append(pagar);
}
return wrap;
}
wrap.id='order-'+e.id;
const detalhe=node('div',null,'detalhe');detalhe.hidden=true;
const alterna=async()=>{
if(!detalhe.hidden){detalhe.hidden=true;ver.textContent='Ver';return;}
await details(e.id,detalhe,e.created_at);detalhe.hidden=false;ver.textContent='Fechar';
};
const ver=button('Ver',alterna);
acao.append(ver);
row.addEventListener('click',event=>{if(!event.target.closest('button,a'))ver.click();});
wrap.append(detalhe);
wrap.abrir=async()=>{if(detalhe.hidden)await alterna();};
return wrap;
}
function filtros(counts,groups){
const box=$('filtros');box.replaceChildren();
const todos=Object.values(counts).reduce((a,b)=>a+b,0);
for(const [key,label] of [['all','Todos'],...Object.entries(groups)]){
const n=key==='all'?todos:counts[key];
const chip=node('button',label+(n?' · '+n:''),'chipF'+(filtro.status===key?' on':''));chip.type='button';
chip.setAttribute('aria-pressed',String(filtro.status===key));
chip.onclick=()=>{filtro={...filtro,status:key,page:1};pedidos().catch(x=>aviso(x.message,true));};
box.append(chip);
}
}
function paginas(total){
const box=$('paginas');box.replaceChildren();
const n=Math.ceil(total/TAMANHO);
if(n<=1){if(total)box.append(node('span',total+(total===1?' pedido':' pedidos'),'nota2'));return;}
const de=(filtro.page-1)*TAMANHO+1, ate=Math.min(total,filtro.page*TAMANHO);
box.append(node('span',de+'–'+ate+' de '+total+' pedidos','nota2'));
const nav=node('div',null,'pags');
const vai=p=>{filtro.page=p;pedidos().then(()=>$('vPedidos').scrollIntoView({block:'start'})).catch(x=>aviso(x.message,true));};
const b=(t,p,label)=>{const x=node('button',t,'pag'+(p===filtro.page?' on':''));x.type='button';if(label)x.setAttribute('aria-label',label);
if(p===filtro.page)x.setAttribute('aria-current','page');else if(p<1||p>n)x.disabled=true;else x.onclick=()=>vai(p);nav.append(x);};
b('‹',filtro.page-1,'Página anterior');
for(let p=Math.max(1,filtro.page-2);p<=Math.min(n,filtro.page+2);p++)b(String(p),p);
b('›',filtro.page+1,'Próxima página');
box.append(nav);
}
async function pedidos(){
$('guest').textContent=conta?'':'Você está vendo os pedidos feitos neste navegador. Entre ou crie uma conta para vê-los em qualquer lugar.';
const q=new URLSearchParams({status:filtro.status,days:String(filtro.days),page:String(filtro.page),size:String(TAMANHO)});
if(filtro.number)q.set('number',filtro.number);
const data=await api('/customer/orders?'+q);states=data.states;
filtros(data.counts,data.groups);
const lista=$('orders');lista.replaceChildren();
if(!data.items.length){
const livre=filtro.status==='all'&&!filtro.number&&!filtro.days;
const vazio=node('div',null,'vazio');
vazio.append(node('p',livre?'Seus pedidos aparecem aqui.':'Nenhum pedido com esse filtro.'));
if(livre){const a=node('a','Enviar uma arte','bPri');a.href='/#envio';vazio.append(a);}
lista.append(vazio);
}
for(const e of data.items)lista.append(linha(e));
paginas(data.total);
const aberto=new URLSearchParams(location.search).get('order');
if(aberto&&/^[0-9a-f-]{36}$/.test(aberto)){
let alvo=$('order-'+aberto);
if(!alvo){
// Not on this page of the list: shown on its own, above it.
try{const o=await api('/customer/orders/'+aberto);
alvo=linha({kind:'order',id:o.id,number:o.number,state:o.state,total_cents:o.snapshot.total_cents,
created_at:o.created_at,items:o.snapshot.items});lista.prepend(alvo);}catch(e){alvo=null;}
}
if(alvo){await alvo.abrir();alvo.scrollIntoView({block:'start'});}
}
}
let buscaTimer;
$('fNumero').addEventListener('input',e=>{e.target.value=soDigitos(e.target.value);clearTimeout(buscaTimer);
buscaTimer=setTimeout(()=>{filtro={...filtro,number:e.target.value,page:1};pedidos().catch(x=>aviso(x.message,true));},350);});
$('fPeriodo').addEventListener('change',e=>{filtro={...filtro,days:Number(e.target.value),page:1};pedidos().catch(x=>aviso(x.message,true));});
function itensPedido(snapshot){
const ul=node('ul',null,'itensPed'); const ul=node('ul',null,'itensPed');
for(const item of snapshot.items){ for(const item of snapshot.items){
const li=node('li'); const li=node('li');
@@ -59,64 +198,18 @@ function itens(snapshot){
if(f&&f.total_cents){const li=node('li',null,'frete');li.append(node('span','Frete','nome'),node('span',f.days?'até '+f.days+' dias úteis':'','qt'),node('b',money(f.total_cents)));ul.append(li);} if(f&&f.total_cents){const li=node('li',null,'frete');li.append(node('span','Frete','nome'),node('span',f.days?'até '+f.days+' dias úteis':'','qt'),node('b',money(f.total_cents)));ul.append(li);}
return ul; return ul;
} }
function entrega(snapshot){
async function load(){ const d=snapshot.destination, p=node('p',null,'entregaPed');
try{ p.append(icone(d?'caminhao':'local'),node('span',d?'Entrega pela Jadlog · '+d.street+', '+d.number+' · '+d.city+'/'+d.state:'Retirada em Franca · Av. Major Elias Motta, 1281'));
const session=await api('/session');scope=session.cart_scope;environment=session.environment; return p;
$('dev-notice').hidden=environment!=='local';
const account=await api('/account/me');
$('auth').hidden=!!account.customer;$('logout').hidden=!account.customer;
$('account').textContent=account.customer?'Conta '+account.customer.mail:'';
$('guest').textContent=account.customer?'':'Você está vendo os pedidos feitos neste navegador. Entre ou crie uma conta para ver seus pedidos em qualquer lugar.';
const data=await api('/customer/orders');states=data.states;
const quotes=$('quotes');quotes.replaceChildren();
if(data.quotes.length){
quotes.append(node('h2','Aguardando pagamento','tituloGrupo'));
for(const quote of data.quotes){
const card=node('article',null,'pedido pendente');
const topo=node('div',null,'pedTopo');
const nome=node('div');nome.append(node('h3','Carrinho de '+dia(quote.created_at)),node('span',quote.approved?'Pronto para pagar':'Em conferência pela equipe','data'));
topo.append(nome);
if(quote.approved)topo.append(node('b',money(quote.approved.total_cents),'valor'));
card.append(topo);
if(quote.approved){const link=node('a','Ir para o pagamento','bPri');link.href='/pagamento?quote='+quote.id;card.append(link);}
else card.append(node('p','A equipe confere a metragem e a nota da arte. Você recebe o valor para pagar em seguida.','nota2'));
quotes.append(card);
} }
}
const orders=$('orders');orders.replaceChildren();
orders.append(node('h2','Pedidos','tituloGrupo'));
if(!data.orders.length){
const vazio=node('div',null,'vazio');
vazio.append(node('p','Nenhum pedido pago ainda.'));
const link=node('a','Enviar uma arte','bPri');link.href='/#envio';vazio.append(link);
orders.append(vazio);
}
const aberto=new URLSearchParams(location.search).get('order');
for(const order of data.orders){
const card=node('article',null,'pedido'+(order.state==='cor'?' correcao':''));card.id='order-'+order.id;
const topo=node('div',null,'pedTopo');
const nome=node('div');nome.append(node('h3','Pedido #'+order.number),node('span',quando(order.created_at),'data'));
topo.append(nome,node('span',states[order.state],'estado est-'+order.state),node('b',money(order.snapshot.total_cents)+(environment==='local'?' · teste':''),'valor'));
card.append(topo);
if(order.state==='cor'){
const p=node('p',null,'pedAlerta');p.append(icone('alerta'),node('span','A equipe precisa de uma correção na sua arte. Abra os detalhes para ver o motivo e enviar o arquivo corrigido.'));
card.append(p);
}else card.append(trilha(order.state));
card.append(itens(order.snapshot),entrega(order.snapshot));
const content=node('div',null,'detalhe');
const ver=button('Ver detalhes e arquivos',async()=>{
if(content.childElementCount){content.replaceChildren();ver.textContent='Ver detalhes e arquivos';return;}
await details(order.id,content,order.created_at);ver.textContent='Fechar detalhes';
},'bLink');
card.append(ver,content);orders.append(card);
if(aberto===order.id){await details(order.id,content,order.created_at);ver.textContent='Fechar detalhes';card.scrollIntoView({block:'start'});}
}
}catch(error){aviso(error.message);}
}
async function details(id,container,criado){ async function details(id,container,criado){
const order=await api('/customer/orders/'+id);container.replaceChildren(); const order=await api('/customer/orders/'+id);container.replaceChildren();
if(order.state==='cor'){
const p=node('p',null,'pedAlerta');p.append(icone('alerta'),node('span','A equipe precisa de uma correção na sua arte. Veja o motivo abaixo e envie o arquivo corrigido.'));
container.append(p);
}else container.append(trilha(order.state));
container.append(itensPedido(order.snapshot),entrega(order.snapshot));
const hist=node('div',null,'secao');hist.append(node('h4','Andamento')); const hist=node('div',null,'secao');hist.append(node('h4','Andamento'));
const ol=node('ol',null,'historico'); const ol=node('ol',null,'historico');
// Payment is where the order starts: the first step is not a movement. // Payment is where the order starts: the first step is not a movement.
@@ -157,12 +250,76 @@ async function details(id,container,criado){
const files=[];for(const [index,input] of inputs.entries())for(const file of input.files)files.push({item_index:index,upload_id:await dtfUpload(file,{api,scope:scope+':correction:'+id+':'+order.version,progress:text=>aviso(text)})}); const files=[];for(const [index,input] of inputs.entries())for(const file of input.files)files.push({item_index:index,upload_id:await dtfUpload(file,{api,scope:scope+':correction:'+id+':'+order.version,progress:text=>aviso(text)})});
if(!files.length)throw new Error('Selecione pelo menos um arquivo corrigido.'); if(!files.length)throw new Error('Selecione pelo menos um arquivo corrigido.');
await api('/customer/orders/'+id+'/corrections',{version:order.version,files,note:note.value}); await api('/customer/orders/'+id+'/corrections',{version:order.version,files,note:note.value});
aviso('Correção enviada. Aguarde a conferência da equipe.');await details(id,container,criado); aviso('Correção enviada. A equipe confere e segue com o pedido.');await details(id,container,criado);
}catch(error){aviso(error.message);}finally{submit.disabled=false;}}; }catch(error){aviso(error.message,true);}finally{submit.disabled=false;}};
container.append(form); container.append(form);
} }
} }
// ---- Overview ------------------------------------------------------------
async function geral(){
$('account').textContent='CNPJ '+fmtCnpj(conta.cnpj)+' · '+conta.mail;
const data=await api('/customer/orders?page=1&size=1');states=data.states;
const box=$('metricas');box.replaceChildren();
for(const [key,label] of [['pay','Aguardando pagamento'],['prod','Em produção'],['cor','Correção'],['fin','Finalizados']]){
if(key==='cor'&&!data.counts.cor)continue;
const a=node('a',null,'metrica'+(key==='cor'?' alerta':''));a.href='/conta/pedidos';a.dataset.view='pedidos';
a.addEventListener('click',()=>{filtro={status:key,number:'',days:0,page:1};});
a.append(node('span',label),node('b',String(data.counts[key])));
box.append(a);
}
const ultimo=$('ultimo');ultimo.replaceChildren();
if(data.items.length)ultimo.append(linha(data.items[0]));
else{const v=node('div',null,'vazio');v.append(node('p','Seu primeiro pedido aparece aqui.'));const a=node('a','Enviar uma arte','bPri');a.href='/#envio';v.append(a);ultimo.append(v);}
}
// ---- Account details -----------------------------------------------------
const CAMPOS_END=[['dNome','recipient'],['dRua','street'],['dNum','number'],['dComp','complement'],['dBairro','district'],['dCidade','city'],['dUf','state']];
function dados(){
$('dCnpj').value=fmtCnpj(conta.cnpj);$('dZap').value=fmtZap(conta.zap);$('dMail').value=conta.mail;
const end=conta.address||{};
$('dCep').value=fmtCep(end.postal_code||'');
for(const [id,k] of CAMPOS_END)$(id).value=end[k]||'';
$('dMailSenha').value='';$('dAtual').value='';$('dNova').value='';
}
$('dZap').addEventListener('input',e=>{e.target.value=fmtZap(soDigitos(e.target.value).slice(0,11));});
$('dCep').addEventListener('input',async e=>{
e.target.value=fmtCep(e.target.value);
const cep=soDigitos(e.target.value);
if(cep.length!==8)return;
try{const a=await api('/cep/'+cep);
for(const [id,v] of [['dRua',a.street],['dBairro',a.district],['dCidade',a.city],['dUf',a.state]])if(v)$(id).value=v;
(['dNome','dRua','dNum'].map($).find(el=>!el.value.trim())||$('dNum')).focus();
}catch(_){/* sem consulta: o cliente preenche */}
});
$('fPerfil').onsubmit=async event=>{
event.preventDefault();
const cep=soDigitos($('dCep').value);
const campos=Object.fromEntries(CAMPOS_END.map(([id,k])=>[k,$(id).value.trim()]));
const algum=cep||Object.values(campos).some(Boolean);
const address=algum?{...campos,state:campos.state.toUpperCase(),postal_code:cep}:null;
try{
const r=await api('/account/profile',{zap:$('dZap').value,address});
conta=r.customer;aviso('Dados salvos.');
}catch(e){aviso(e.status===422?(algum?'Confira o WhatsApp e o endereço: todos os campos, menos o complemento, são necessários.':'Confira o WhatsApp.'):e.message,true);}
};
$('fEmail').onsubmit=async event=>{
event.preventDefault();
try{const r=await api('/account/email',{email:$('dMail').value,password:$('dMailSenha').value});
conta=r.customer;$('dMailSenha').value='';$('quem').textContent=conta.mail;aviso('E-mail alterado. Use o novo e-mail para entrar.');}
catch(e){aviso(e.status===422?'Confira o e-mail.':e.message,true);}
};
$('fSenha').onsubmit=async event=>{
event.preventDefault();
try{await api('/account/password',{current:$('dAtual').value,new:$('dNova').value});
$('dAtual').value='';$('dNova').value='';aviso('Senha alterada.');}
catch(e){aviso(e.status===422?'A nova senha precisa ter 12 caracteres ou mais.':e.message,true);}
};
// ---- Signing in ----------------------------------------------------------
async function migrateGuestCart(previous,next){ async function migrateGuestCart(previous,next){
if(previous===next)return; if(previous===next)return;
const database=await new Promise((resolve,reject)=>{const r=indexedDB.open('dtf-local-cart',1);r.onupgradeneeded=()=>r.result.createObjectStore('cart');r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error);}); const database=await new Promise((resolve,reject)=>{const r=indexedDB.open('dtf-local-cart',1);r.onupgradeneeded=()=>r.result.createObjectStore('cart');r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error);});
@@ -170,14 +327,25 @@ async function migrateGuestCart(previous,next){
database.close(); database.close();
} }
async function authenticate(path,body){ async function authenticate(path,body){
const previous=scope,guest=!(await api('/account/me')).customer; const previous=scope,guest=!conta;
await api(path,body);forgetCheckout();aviso('Acesso confirmado.'); const r=await api(path,body);forgetCheckout();
const next=(await api('/session')).cart_scope; const next=(await api('/session')).cart_scope;scope=next;
if(guest)try{await migrateGuestCart(previous,next);}catch{aviso('Acesso confirmado. Não foi possível transferir o carrinho salvo neste navegador.');} if(guest)try{await migrateGuestCart(previous,next);}catch{aviso('Não foi possível transferir o carrinho salvo neste navegador.',true);}
await load();$('password').value='';$('register-password').value=''; conta=r.customer;$('quem').textContent=conta.mail;
$('password').value='';$('register-password').value='';
await mostra();
} }
$('login').onsubmit=async event=>{event.preventDefault();try{await authenticate('/account/login',{email:$('email').value,password:$('password').value});}catch(e){aviso(e.message);}}; $('login').onsubmit=async event=>{event.preventDefault();try{await authenticate('/account/login',{email:$('email').value,password:$('password').value});}catch(e){aviso(e.message,true);}};
$('register').onsubmit=async event=>{event.preventDefault();try{await authenticate('/account/register',{customer:{cnpj:$('cnpj').value,zap:$('phone').value,mail:$('register-email').value},password:$('register-password').value});}catch(e){aviso(e.message);}}; $('register').onsubmit=async event=>{event.preventDefault();try{await authenticate('/account/register',{customer:{cnpj:$('cnpj').value,zap:$('phone').value,mail:$('register-email').value},password:$('register-password').value});}catch(e){aviso(e.status===422?'Confira o CNPJ, o WhatsApp e o e-mail. A senha precisa ter 12 caracteres ou mais.':e.message,true);}};
$('logout').onclick=async()=>{try{await window.dtfForgetPrivateData();await api('/account/logout',{});location.reload();}catch(error){aviso('Não foi possível limpar os dados deste navegador. Feche as abas do Site e limpe os dados do site nas configurações do navegador.');}}; $('esqueci').onclick=()=>{$('esqueciTx').hidden=!$('esqueciTx').hidden;};
$('refresh').onclick=load; $('logout').onclick=async()=>{try{await window.dtfForgetPrivateData();await api('/account/logout',{});location.assign('/conta/entrar');}catch(error){aviso('Não foi possível limpar os dados deste navegador. Feche as abas do Site e limpe os dados do site nas configurações do navegador.',true);}};
load();
(async()=>{
try{
const session=await api('/session');scope=session.cart_scope;environment=session.environment;
$('dev-notice').hidden=environment!=='local';
conta=(await api('/account/me')).customer;
if(conta)$('quem').textContent=conta.mail;
await mostra();
}catch(error){aviso(error.message,true);}
})();