first push message

This commit is contained in:
2026-07-01 14:41:49 +07:00
parent 6667dec2bf
commit 58b5f46cc4
2951 changed files with 316619 additions and 0 deletions
@@ -0,0 +1,70 @@
/** @odoo-module **/
// ^^^ MUST be first line, no comments/spaces before
import { Component, useState, onWillStart } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { jsonrpc } from "@web/core/network/rpc_service";
import { useNotification } from "@web/core/notifications/notification_hook";
export class AppSelector extends Component {
static template = "custom_subscriptions.AppSelectorTemplate";
setup() {
this.orm = useService("orm");
this.notification = useNotification();
this.state = useState({
categories: {},
selectedIds: [],
loading: true
});
onWillStart(async () => {
await this.fetchApps();
});
}
async fetchApps() {
try {
const result = await jsonrpc("/custom_subscriptions/get_available_apps");
this.state.categories = result;
this.state.loading = false;
} catch (error) {
console.error("Failed to load apps:", error);
this.notification.add("Error loading apps", { type: "danger" });
}
}
toggleSelection(id) {
const idx = this.state.selectedIds.indexOf(id);
if (idx === -1) {
this.state.selectedIds.push(id);
} else {
this.state.selectedIds.splice(idx, 1);
}
}
isSelected(id) {
return this.state.selectedIds.includes(id);
}
async installApps() {
if (this.state.selectedIds.length === 0) {
this.notification.add("Please select at least one app.", { type: "warning" });
return;
}
if (!confirm(`Install ${this.state.selectedIds.length} app(s)?`)) return;
try {
await this.orm.call("ir.module.module", "button_immediate_install", [this.state.selectedIds]);
this.notification.add("Installing... Reloading soon", { type: "success" });
setTimeout(() => window.location.reload(), 2000);
} catch (error) {
this.notification.add(error.data?.message || "Install failed", { type: "danger" });
}
}
}
// ⚠️ CRITICAL: This key MUST match the <field name="tag"> in XML exactly
registry.category("actions").add("custom_app_selector", AppSelector);
@@ -0,0 +1,193 @@
.app-selector-wrapper {
background-color: #f8f9fa;
min-height: 100vh;
padding-bottom: 40px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.app-selector-header {
text-align: center;
padding: 30px 20px;
background: white;
margin-bottom: 20px;
border-bottom: 1px solid #eee;
h1 {
font-size: 2.2rem;
margin: 0 0 8px 0;
color: #212529;
}
p {
color: #717b84;
margin: 0;
}
}
.app-selector-content {
display: flex;
max-width: 1200px;
margin: 0 auto;
gap: 30px;
padding: 0 20px;
}
.app-grid {
flex: 1;
min-width: 0;
}
.category-title {
font-style: italic;
font-weight: 400;
font-size: 1.3rem;
margin: 25px 0 15px 0;
color: #000;
border-bottom: 1px solid #eee;
padding-bottom: 8px;
}
.app-cards-row {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
gap: 12px;
}
.app-card {
background: white;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 16px 12px;
position: relative;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
&:hover {
border-color: #adb5bd;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
transform: translateY(-2px);
}
&.selected {
border: 2px solid #714B67;
background-color: #f9f5fa;
}
}
.card-check {
display: none;
position: absolute;
top: -8px;
right: -8px;
width: 22px;
height: 22px;
background: #714B67;
color: white;
border-radius: 50%;
align-items: center;
justify-content: center;
font-size: 11px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.app-card.selected .card-check {
display: flex;
}
.app-icon {
width: 42px;
height: 42px;
margin-bottom: 10px;
object-fit: contain;
}
.app-name {
font-weight: 600;
font-size: 0.9rem;
margin-bottom: 4px;
color: #212529;
line-height: 1.3;
}
.app-desc {
font-size: 0.75rem;
color: #717b84;
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
max-height: 2.6em;
}
/* Sidebar */
.app-sidebar {
width: 280px;
flex-shrink: 0;
}
.sidebar-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
position: sticky;
top: 20px;
}
.sidebar-card h3 {
margin: 0 0 15px 0;
padding-bottom: 12px;
border-bottom: 1px solid #eee;
font-size: 1.1rem;
}
.trial-info {
background-color: #e6f4f9;
padding: 14px;
border-radius: 6px;
margin: 15px 0;
font-size: 0.85rem;
color: #0c5460;
line-height: 1.5;
p { margin: 4px 0; }
}
.btn-continue {
width: 100%;
background-color: #714B67;
color: white;
border: none;
padding: 12px 16px;
font-weight: 600;
font-size: 1rem;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
&:hover:not(:disabled) {
background-color: #5a3c53;
}
&:disabled {
background-color: #adb5bd;
cursor: not-allowed;
}
}
/* Responsive */
@media (max-width: 992px) {
.app-selector-content {
flex-direction: column;
}
.app-sidebar {
width: 100%;
}
.sidebar-card {
position: static;
}
}
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="custom_subscriptions.AppSelectorTemplate" owl="1">
<div class="app-selector-wrapper">
<div class="app-selector-header">
<h1>Choose your Apps</h1>
<p>Free instant access. No credit card required.</p>
</div>
<div class="app-selector-content">
<!-- App Grid -->
<div class="app-grid">
<t t-if="state.loading">
<div class="text-center p-5 text-muted">Loading apps...</div>
</t>
<t t-else="">
<t t-foreach="Object.keys(state.categories)" t-as="category" t-key="category">
<div class="category-section">
<h2 class="category-title" t-esc="category"/>
<div class="app-cards-row">
<t t-foreach="state.categories[category]" t-as="app" t-key="app.id">
<div class="app-card"
t-att-class="isSelected(app.id) ? 'selected' : ''"
t-on-click="() => this.toggleSelection(app.id)">
<div class="card-check">
<i class="fa fa-check"/>
</div>
<div class="card-body">
<img t-att-src="app.icon" class="app-icon" alt="" onerror="this.src='/web/static/img/placeholder.png'"/>
<div class="app-name" t-esc="app.name"/>
<div class="app-desc" t-esc="app.shortdesc"/>
</div>
</div>
</t>
</div>
</div>
</t>
</t>
</div>
<!-- Sidebar -->
<div class="app-sidebar">
<div class="sidebar-card">
<h3><t t-esc="state.selectedIds.length"/> Apps selected</h3>
<div class="trial-info">
<p><strong>Community Edition</strong></p>
<p>Install modules instantly.</p>
<p>No credit card required.</p>
</div>
<button class="btn-continue" t-on-click="installApps" t-att-disabled="state.selectedIds.length === 0">
Continue
</button>
</div>
</div>
</div>
</div>
</t>
</templates>