F-201 F-202 F-203: POS cash close report + email + PIN admin
F-201: GET /pos/reports/cash-close/:id with financial summary, sales by state, payments breakdown, items sold. Extended /pos/sessions/:id. F-202: Cash close email sent on session close to smtpReportEmail (best-effort). smtpReportEmail field added to admin SMTP settings. F-203: Admin POS terminal config: selfpayMode, closeSessionRequiresPin, closeSessionPin (4-6 digits) with dedicated settings section.
This commit is contained in:
@@ -211,6 +211,33 @@ export default function PosAdminPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// F-203: save selfpay + PIN settings via PATCH /pos/admin/terminals/:id
|
||||
const saveSecuritySettings = async () => {
|
||||
if (!configuring) return;
|
||||
setSavingSecurity(true);
|
||||
setSecurityMessage('');
|
||||
try {
|
||||
const settings: Record<string, unknown> = {
|
||||
selfpayMode,
|
||||
closeSessionRequiresPin: closePinRequired,
|
||||
};
|
||||
if (closePinValue) settings.closeSessionPin = closePinValue;
|
||||
await api.patch(`/api/pos/admin/terminals/${configuring.id}`, { settings });
|
||||
setSecurityMessage('Configuración de cierre guardada');
|
||||
setTerminals(prev =>
|
||||
prev.map(t =>
|
||||
t.id === configuring.id
|
||||
? { ...t, settings: { ...t.settings, ...settings } }
|
||||
: t,
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
setSecurityMessage('Error al guardar: ' + (err instanceof Error ? err.message : 'Error'));
|
||||
} finally {
|
||||
setSavingSecurity(false);
|
||||
}
|
||||
};
|
||||
|
||||
const createPaymentMethod = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
setSavingMethod(true);
|
||||
@@ -600,16 +627,55 @@ export default function PosAdminPage() {
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-5 flex items-center gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void saveTouchConfig()}
|
||||
disabled={savingTouch}
|
||||
className="rounded-xl bg-[#2D6A4F] px-5 py-2.5 text-sm font-semibold text-white disabled:opacity-50"
|
||||
>
|
||||
{savingTouch ? 'Guardando…' : 'Guardar terminal'}
|
||||
</button>
|
||||
{touchMessage && <p className="text-sm">{touchMessage}</p>}
|
||||
{/* F-203: cash close security settings */}
|
||||
<div className="mt-6 border-t pt-4">
|
||||
<h3 className="mb-3 text-sm font-bold text-gray-900">Seguridad de cierre de caja</h3>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<label className="flex items-center gap-3 rounded-xl bg-gray-50 p-4 text-sm font-medium">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selfpayMode}
|
||||
onChange={(e) => setSelfpayMode(e.target.checked)}
|
||||
className="h-5 w-5 accent-[#2D6A4F]"
|
||||
/>
|
||||
Modo autopago (oculta botón de cierre)
|
||||
</label>
|
||||
<label className="flex items-center gap-3 rounded-xl bg-gray-50 p-4 text-sm font-medium">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={closePinRequired}
|
||||
onChange={(e) => setClosePinRequired(e.target.checked)}
|
||||
className="h-5 w-5 accent-[#2D6A4F]"
|
||||
/>
|
||||
Requerir PIN para cerrar caja
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<label className="mb-1 block text-sm font-medium text-gray-700">
|
||||
PIN de cajero {closePinRequired && <span className="text-red-500">*</span>}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
inputMode="numeric"
|
||||
maxLength={6}
|
||||
value={closePinValue}
|
||||
onChange={(e) => setClosePinValue(e.target.value)}
|
||||
placeholder="4-6 dígitos"
|
||||
className="w-full rounded-xl border border-gray-300 px-3 py-2 text-sm focus:border-[#2D6A4F] focus:outline-none"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">Este PIN se requiere para autorizar el cierre de caja.</p>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void saveSecuritySettings()}
|
||||
disabled={savingSecurity}
|
||||
className="rounded-xl bg-[#2D6A4F] px-5 py-2 text-sm font-semibold text-white disabled:opacity-50"
|
||||
>
|
||||
{savingSecurity ? 'Guardando…' : 'Guardar seguridad'}
|
||||
</button>
|
||||
{securityMessage && <p className="text-sm">{securityMessage}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
@@ -817,7 +883,7 @@ function Field({
|
||||
{label}
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
value={value ?? ""}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
required={required}
|
||||
min={min}
|
||||
|
||||
@@ -198,6 +198,11 @@ export default function SettingsPage() {
|
||||
{field('smtpUser', 'Usuario / cuenta de correo', { type: 'email', placeholder: 'info@mercadodevida.es' })}
|
||||
{field('smtpPass', 'Contraseña SMTP', { type: 'password', placeholder: form?.smtpPassConfigured ? 'Contraseña configurada (escribe para reemplazar)' : 'Contraseña del buzón' })}
|
||||
{field('smtpFrom', 'Remitente', { type: 'email', placeholder: 'info@mercadodevida.es' })}
|
||||
<div className="border-t border-gray-200 pt-4 mt-2">
|
||||
<h3 className="mb-3 text-sm font-semibold text-gray-800">Reportes automáticos</h3>
|
||||
{field('smtpReportEmail', 'Email destino de reportes', { type: 'email', placeholder: 'direccion@ejemplo.com' })}
|
||||
<p className="text-xs text-gray-400 -mt-2">Recibirás el reporte de cierre de caja por email al cerrar cada sesión.</p>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user