294 lines
20 KiB
XML
294 lines
20 KiB
XML
<?xml version="1.0" encoding="utf-8"?>
|
|
<templates xml:space="preserve">
|
|
<t t-name="hr_expense_account_split.KioskApp">
|
|
<div class="o_expense_kiosk_container d-flex flex-column h-100 bg-light">
|
|
<header class="bg-primary text-white p-3 d-flex justify-content-between align-items-center shadow-sm">
|
|
<h2 class="m-0">Expense Kiosk</h2>
|
|
<button t-if="state.screen !== 'employee_selection'" class="btn btn-light" t-on-click="backToSelection">
|
|
<i class="fa fa-home me-2"/>Home
|
|
</button>
|
|
</header>
|
|
|
|
<main class="flex-grow-1 p-4 overflow-auto">
|
|
<!-- SUCCESS SCREEN -->
|
|
<div t-if="state.screen === 'success'" class="text-center p-5 animate-fade-in">
|
|
<div class="display-1 text-success mb-3"><i class="fa fa-check-circle"/></div>
|
|
<h3>Successfully Submitted!</h3>
|
|
<p class="text-muted">Returning to home in 3 seconds...</p>
|
|
</div>
|
|
|
|
<!-- EMPLOYEE SELECTION -->
|
|
<div t-if="state.screen === 'employee_selection'" class="animate-fade-in">
|
|
<div class="mb-4 d-flex justify-content-center">
|
|
<div class="input-group w-50 shadow-sm">
|
|
<span class="input-group-text"><i class="fa fa-search"/></span>
|
|
<input type="text" class="form-control form-control-lg" placeholder="Search your name..." t-model="state.searchQuery"/>
|
|
</div>
|
|
</div>
|
|
<div class="row row-cols-2 row-cols-md-4 g-4">
|
|
<t t-foreach="filteredEmployees" t-as="emp" t-key="emp.id">
|
|
<div class="col">
|
|
<div class="card h-100 text-center shadow-sm border-0 cursor-pointer hover-shadow" t-on-click="() => this.selectEmployee(emp)">
|
|
<div class="p-4 bg-white rounded-top">
|
|
<img t-att-src="emp.avatar_url" class="rounded-circle mb-3 border border-3 border-light" style="width: 100px; height: 100px; object-fit: cover;"/>
|
|
<h5 class="card-title text-truncate" t-esc="emp.name"/>
|
|
<small class="text-muted" t-esc="emp.job_name"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</t>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PIN PAD -->
|
|
<div t-if="state.screen === 'pin_pad'" class="d-flex flex-column align-items-center animate-fade-in">
|
|
<div class="text-center mb-4">
|
|
<img t-att-src="state.selectedEmployee.avatar_url" class="rounded-circle mb-3" style="width: 120px; height: 120px; object-fit: cover;"/>
|
|
<h3>Welcome, <t t-esc="state.selectedEmployee.name"/></h3>
|
|
<p class="text-muted">Enter your PIN to continue</p>
|
|
</div>
|
|
<div class="pin-display display-4 mb-4 d-flex gap-2">
|
|
<t t-foreach="[0,1,2,3]" t-as="i" t-key="i">
|
|
<span t-att-class="state.enteredPin.length > i ? 'text-primary' : 'text-muted'">●</span>
|
|
</t>
|
|
</div>
|
|
<div class="pin-pad row row-cols-3 g-3" style="max-width: 300px;">
|
|
<t t-foreach="[1,2,3,4,5,6,7,8,9]" t-as="i" t-key="i">
|
|
<div class="col"><button class="btn btn-outline-secondary w-100 py-4 fs-3 shadow-none" t-on-click="() => this.pressKey(i)" t-esc="i"/></div>
|
|
</t>
|
|
<div class="col"><button class="btn btn-outline-secondary w-100 py-4 fs-3 shadow-none" t-on-click="clearPin"><i class="fa fa-times text-danger"/></button></div>
|
|
<div class="col"><button class="btn btn-outline-secondary w-100 py-4 fs-3 shadow-none" t-on-click="() => this.pressKey(0)">0</button></div>
|
|
<div class="col"><button class="btn btn-outline-secondary w-100 py-4 fs-3 shadow-none disabled"><i class="fa fa-check text-success"/></button></div>
|
|
</div>
|
|
<button class="btn btn-link mt-4 text-muted" t-on-click="backToSelection">Cancel</button>
|
|
</div>
|
|
|
|
<!-- ACTION SELECTION -->
|
|
<div t-if="state.screen === 'action_selection'" class="d-flex flex-column align-items-center animate-fade-in h-100 justify-content-center">
|
|
<h3 class="mb-5">What would you like to do?</h3>
|
|
<div class="d-flex gap-5">
|
|
<div class="action-card card text-center p-5 shadow border-0 cursor-pointer rounded-4 hover-lift" t-on-click="() => this.selectAction('realization')" style="width: 250px;">
|
|
<div class="display-3 text-success mb-3"><i class="fa fa-file-image-o"/></div>
|
|
<h5>Upload Receipt</h5>
|
|
<p class="text-muted small">Realize an existing expense report</p>
|
|
<span t-if="state.pendingRealizations.length > 0" class="badge rounded-pill bg-danger position-absolute top-0 start-100 translate-middle" t-esc="state.pendingRealizations.length"/>
|
|
</div>
|
|
<div class="action-card card text-center p-5 shadow border-0 cursor-pointer rounded-4 hover-lift" t-on-click="() => this.selectAction('new_expense')" style="width: 250px;">
|
|
<div class="display-3 text-primary mb-3"><i class="fa fa-plus-circle"/></div>
|
|
<h5>New Expense</h5>
|
|
<p class="text-muted small">Submit a new reimbursement request</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Submissions List -->
|
|
<div class="mt-5 w-100 px-3" t-if="state.submittedExpenses.length > 0">
|
|
<h4 class="mb-3 text-start"><i class="fa fa-history me-2"></i>Your Recent Submissions</h4>
|
|
<div class="table-responsive rounded shadow-sm bg-white">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th class="ps-3">Reference / Description</th>
|
|
<th>Date</th>
|
|
<th class="text-end">Total</th>
|
|
<th class="text-center">Status</th>
|
|
<th class="text-center pe-3">Payment</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<t t-foreach="state.submittedExpenses" t-as="exp" t-key="exp.id">
|
|
<tr>
|
|
<td class="ps-3">
|
|
<div class="fw-bold text-primary" t-esc="exp.sequences"/>
|
|
<div class="small text-muted" t-esc="exp.name"/>
|
|
</td>
|
|
<td t-esc="exp.date"/>
|
|
<td class="text-end fw-bold" t-esc="exp.total_amount"/>
|
|
<td class="text-center">
|
|
<span t-attf-class="badge rounded-pill #{['approved', 'paid', 'posted'].includes(exp.state_raw) ? 'bg-success' : (['submitted', 'reported'].includes(exp.state_raw) ? 'bg-info' : (exp.state_raw === 'refused' ? 'bg-danger' : 'bg-warning'))}" t-esc="exp.state"/>
|
|
</td>
|
|
<td class="text-center pe-3">
|
|
<span t-attf-class="badge rounded-pill #{exp.payment_state_raw === 'paid' ? 'bg-success' : (exp.payment_state_raw === 'in_payment' ? 'bg-info' : (exp.payment_state_raw === 'not_paid' ? 'bg-secondary' : 'bg-warning'))}" t-esc="exp.payment_status"/>
|
|
</td>
|
|
</tr>
|
|
</t>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="text-muted small mt-2 text-start">
|
|
<i class="fa fa-info-circle me-1"></i> Showing your last 15 submissions.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PAYMENT MODE SELECTION -->
|
|
<div t-if="state.screen === 'payment_mode_selection'" class="d-flex flex-column align-items-center animate-fade-in h-100 justify-content-center">
|
|
<h3 class="mb-5">How is this expense paid?</h3>
|
|
<div class="d-flex gap-5">
|
|
<div class="action-card card text-center p-5 shadow border-0 cursor-pointer rounded-4 hover-lift" t-on-click="() => this.selectPaymentMode('company_account')" style="width: 250px;">
|
|
<div class="display-3 text-info mb-3"><i class="fa fa-money"/></div>
|
|
<h5>Kasbon</h5>
|
|
<p class="text-muted small">Paid by Company</p>
|
|
</div>
|
|
<div class="action-card card text-center p-5 shadow border-0 cursor-pointer rounded-4 hover-lift" t-on-click="() => this.selectPaymentMode('own_account')" style="width: 250px;">
|
|
<div class="display-3 text-warning mb-3"><i class="fa fa-credit-card"/></div>
|
|
<h5>Reimburse</h5>
|
|
<p class="text-muted small">Paid by Employee</p>
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-link mt-4 text-muted" t-on-click="() => this.state.screen = 'action_selection'">Back</button>
|
|
</div>
|
|
|
|
<!-- CATEGORY SELECTION -->
|
|
<div t-if="state.screen === 'category_selection'" class="animate-fade-in">
|
|
<div class="text-center mb-4">
|
|
<h3>Select Expense Category</h3>
|
|
<p class="text-muted">What type of expense is this?</p>
|
|
</div>
|
|
<div class="mb-4 d-flex justify-content-center">
|
|
<div class="input-group w-50 shadow-sm">
|
|
<span class="input-group-text"><i class="fa fa-search"/></span>
|
|
<input type="text" class="form-control" placeholder="Search category..." t-model="state.categorySearchQuery"/>
|
|
</div>
|
|
</div>
|
|
<div class="row row-cols-2 row-cols-md-4 g-4 overflow-auto px-2" style="max-height: 60vh;">
|
|
<t t-foreach="filteredCategories" t-as="cat" t-key="cat.id">
|
|
<div class="col">
|
|
<div class="card h-100 text-center shadow-sm border-0 cursor-pointer hover-shadow" t-on-click="() => this.selectCategory(cat)">
|
|
<div class="p-3">
|
|
<img t-att-src="cat.image_url" class="mb-2" style="width: 64px; height: 64px; object-fit: contain;"/>
|
|
<div class="fw-bold small text-truncate" t-esc="cat.display_name"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</t>
|
|
</div>
|
|
<div class="text-center mt-4">
|
|
<button class="btn btn-link text-muted" t-on-click="() => this.state.screen = 'payment_mode_selection'">Back</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- FORM -->
|
|
<div t-if="state.screen === 'form'" class="animate-fade-in" style="max-width: 600px; margin: 0 auto;">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h3 class="m-0">
|
|
<t t-if="state.selectedAction === 'realization'">Realize Receipt</t>
|
|
<t t-else="">Create New Expense</t>
|
|
</h3>
|
|
<span t-if="state.selectedCategory" class="badge bg-primary fs-6 py-2 px-3">
|
|
<i class="fa fa-tag me-1"/> <t t-esc="state.selectedCategory.display_name"/>
|
|
</span>
|
|
</div>
|
|
|
|
<div t-if="state.selectedAction === 'realization'" class="mb-4">
|
|
<label class="form-label fw-bold">Select Expense to Realize</label>
|
|
<div class="list-group shadow-sm">
|
|
<t t-foreach="state.pendingRealizations" t-as="exp" t-key="exp.id">
|
|
<button type="button"
|
|
t-attf-class="list-group-item list-group-item-action d-flex justify-content-between align-items-center #{state.selectedExpense and state.selectedExpense.id === exp.id ? 'active' : ''}"
|
|
t-on-click="() => this.selectExpense(exp)">
|
|
<div>
|
|
<div class="fw-bold" t-esc="exp.name"/>
|
|
<small t-esc="exp.date"/>
|
|
</div>
|
|
<span class="badge bg-light text-dark shadow-sm">
|
|
<t t-esc="exp.total_amount"/>
|
|
</span>
|
|
</button>
|
|
</t>
|
|
</div>
|
|
</div>
|
|
|
|
<div t-foreach="state.formData.lines" t-as="line" t-key="line_index" class="card shadow-sm border-0 p-4 mb-4 position-relative animate-fade-in">
|
|
<button t-if="state.formData.lines.length > 1" class="btn btn-sm btn-outline-danger position-absolute top-0 end-0 m-2 border-0" t-on-click="() => this.removeLine(line_index)">
|
|
<i class="fa fa-times-circle fs-5"/>
|
|
</button>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold">Amount <small t-if="state.formData.lines.length > 1" class="text-muted">(Receipt #<t t-esc="line_index + 1"/>)</small></label>
|
|
<div class="input-group input-group-lg">
|
|
<span class="input-group-text">Rp</span>
|
|
<input type="number" class="form-control text-center py-3" t-model.number="line.amount" placeholder="0.00"/>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label fw-bold">Description</label>
|
|
<input type="text" class="form-control" t-model="line.description" placeholder="e.g. Lunch with Client"/>
|
|
</div>
|
|
<div class="mb-3 text-center">
|
|
<label class="form-label d-block text-start fw-bold">
|
|
Upload Receipt Image
|
|
<t t-if="state.selectedAction === 'new_expense'">
|
|
<span t-if="state.selectedPaymentMode === 'own_account'" class="text-danger ms-1">*</span>
|
|
<span t-else="" class="text-muted fw-normal ms-1">(Optional)</span>
|
|
</t>
|
|
<t t-else="">
|
|
<span class="text-danger ms-1">*</span>
|
|
</t>
|
|
</label>
|
|
<div class="receipt-preview-container position-relative mb-3 d-flex justify-content-center border-dashed" style="height: 200px; border: 2px dashed #dee2e6; background-color: white !important;">
|
|
<img t-if="line.image" t-att-src="line.image" class="h-100 rounded" style="object-fit: contain;"/>
|
|
<div t-else="" class="d-flex flex-column justify-content-center text-muted">
|
|
<i class="fa fa-camera display-4 mb-2"/>
|
|
<span>Take a photo or upload file</span>
|
|
</div>
|
|
<input type="file" accept="image/*" class="opacity-0 position-absolute top-0 start-0 w-100 h-100 cursor-pointer" t-on-change="(ev) => this.onLineFileChange(line_index, ev)"/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ADD ANOTHER RECEIPT BUTTON -->
|
|
<div t-if="state.selectedAction === 'realization'" class="text-center mb-4">
|
|
<button class="btn btn-outline-primary shadow-sm px-4" t-on-click="addLine">
|
|
<i class="fa fa-plus-circle me-2"/>Add Another Receipt
|
|
</button>
|
|
</div>
|
|
|
|
<!-- TOTAL SUMMARY -->
|
|
<div t-if="state.formData.lines.length > 1" class="card shadow-sm border-0 p-3 mb-4 bg-info bg-opacity-10">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<span class="fw-bold text-info">Total Realization:</span>
|
|
<span class="fs-4 fw-bold text-info">
|
|
Rp <t t-esc="state.formData.lines.reduce((s, l) => s + (l.amount || 0), 0).toLocaleString()"/>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex gap-3">
|
|
<button class="btn btn-outline-secondary btn-lg flex-grow-1" t-on-click="() => this.state.screen = state.selectedAction === 'new_expense' ? 'category_selection' : 'action_selection'">Back</button>
|
|
<button class="btn btn-primary btn-lg flex-grow-1 py-3 shadow" t-on-click="submitForm">
|
|
<t t-if="state.selectedAction === 'realization'">Submit Realization</t>
|
|
<t t-else="">Submit Expense</t>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
<footer class="bg-white p-2 text-center text-muted border-top small">
|
|
Odoo 17 Expense Kiosk — Kipas Lima Delapan
|
|
</footer>
|
|
</div>
|
|
|
|
<style>
|
|
.hover-shadow:hover { box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important; }
|
|
.hover-lift:hover { transform: translateY(-5px); transition: transform .2s; }
|
|
.cursor-pointer { cursor: pointer; }
|
|
.o_expense_kiosk_container {
|
|
min-height: 100vh;
|
|
overflow-x: hidden;
|
|
background-color: #f8f9fa !important;
|
|
color: #212529 !important;
|
|
}
|
|
.card { background-color: white !important; color: #212529 !important; }
|
|
.list-group-item { background-color: white !important; color: #212529 !important; }
|
|
.list-group-item.active { background-color: var(--primary) !important; color: white !important; border-color: var(--primary) !important; }
|
|
.pin-pad button { background-color: white !important; color: #212529 !important; }
|
|
.pin-pad button:hover { background-color: #e9ecef !important; }
|
|
.input-group-text, .form-control { background-color: white !important; color: #212529 !important; }
|
|
.animate-fade-in { animation: fadeIn 0.3s ease-in-out; }
|
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
|
|
.pin-display { font-family: monospace; letter-spacing: 15px; }
|
|
.list-group-item.active { background-color: var(--primary); border-color: var(--primary); }
|
|
</style>
|
|
</t>
|
|
</templates>
|