g360-cli 1.13.1 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +71 -71
- package/src/assets/config/g360-skills.json +165 -0
- package/src/assets/config/project-types.json +23 -4
- package/src/assets/snippets/snippets.json +4 -0
- package/src/assets/templates/lit-web/src/index.js +1 -1
- package/src/assets/templates/python-cli/src/core/__init__.py +0 -0
- package/src/assets/templates/python-flet/README.md +6 -1
- package/src/assets/templates/python-flet/pyproject.toml +2 -2
- package/src/assets/templates/python-flet/src/core/skill.json +1 -1
- package/src/assets/templates/python-flet-migrate/pyproject.toml +1 -3
- package/src/assets/templates/python-flet-polished/README.md +170 -0
- package/src/assets/templates/python-flet-polished/assets/data/sample_data.json +12 -0
- package/src/assets/templates/python-flet-polished/assets/fonts/Inter-Variable.ttf +0 -0
- package/src/assets/templates/python-flet-polished/assets/fonts/JetBrainsMono-Variable.ttf +0 -0
- package/src/assets/templates/python-flet-polished/assets/images/app.ico +0 -0
- package/src/assets/templates/python-flet-polished/assets/images/logo.svg +9 -0
- package/src/assets/templates/python-flet-polished/build-portable.bat +42 -0
- package/src/assets/templates/python-flet-polished/create_shortcut.vbs +15 -0
- package/src/assets/templates/python-flet-polished/g360_flet/__init__.py +0 -0
- package/src/assets/templates/python-flet-polished/g360_flet/g360_signature.py +210 -0
- package/src/assets/templates/python-flet-polished/launch.vbs +3 -0
- package/src/assets/templates/python-flet-polished/launch_minimized.bat +3 -0
- package/src/assets/templates/python-flet-polished/main.py +47 -0
- package/src/assets/templates/python-flet-polished/pyproject.toml +18 -0
- package/src/assets/templates/python-flet-polished/requirements.txt +3 -0
- package/src/assets/templates/python-flet-polished/run.bat +189 -0
- package/src/assets/templates/python-flet-polished/skill.json +28 -0
- package/src/assets/templates/python-flet-polished/src/__init__.py +0 -0
- package/src/assets/templates/python-flet-polished/src/app.py +343 -0
- package/src/assets/templates/python-flet-polished/src/config/__init__.py +0 -0
- package/src/assets/templates/python-flet-polished/src/config/theme.py +122 -0
- package/src/assets/templates/python-flet-polished/src/core/__init__.py +0 -0
- package/src/assets/templates/python-flet-polished/src/core/constants.py +49 -0
- package/src/assets/templates/python-flet-polished/src/core/processor.py +128 -0
- package/src/assets/templates/python-flet-polished/src/core/skill.json +40 -0
- package/src/assets/templates/python-flet-polished/src/ui/__init__.py +0 -0
- package/src/assets/templates/python-flet-polished/src/ui/dashboard.py +308 -0
- package/src/assets/templates/python-flet-polished/src/ui/kpi_card.py +69 -0
- package/src/assets/templates/python-flet-polished/src/ui/search_overlay.py +143 -0
- package/src/assets/templates/python-flet-polished/sync_portable.py +71 -0
- package/src/assets/templates/solid-web/src/components/App.jsx +2 -2
- package/src/cli.js +4 -4
- package/src/commands/clean.js +0 -12
- package/src/commands/docs.js +105 -1
- package/src/commands/ingest.js +11 -61
- package/src/commands/init.js +34 -3
- package/src/commands/lint.js +35 -77
- package/src/commands/scan.js +9 -37
- package/src/commands/validate.js +6 -36
- package/src/lib/file-utils.js +89 -2
- package/src/lib/python-runner.js +77 -0
- package/py/src/g360_core/__pycache__/__init__.cpython-312.pyc +0 -0
- package/py/src/g360_core/__pycache__/__init__.cpython-314.pyc +0 -0
- package/py/src/g360_core/__pycache__/batch_processor.cpython-312.pyc +0 -0
- package/py/src/g360_core/__pycache__/batch_processor.cpython-314.pyc +0 -0
- package/py/src/g360_core/__pycache__/commercial_engine.cpython-314.pyc +0 -0
- package/py/src/g360_core/__pycache__/logger.cpython-312.pyc +0 -0
- package/py/src/g360_core/__pycache__/logger.cpython-314.pyc +0 -0
- package/py/src/g360_core/__pycache__/pipeline.cpython-312.pyc +0 -0
- package/py/src/g360_core/__pycache__/pipeline.cpython-314.pyc +0 -0
- package/py/src/g360_core/__pycache__/processor.cpython-312.pyc +0 -0
- package/py/src/g360_core/__pycache__/processor.cpython-314.pyc +0 -0
- package/py/src/g360_core/__pycache__/processor_segmentacion.cpython-312.pyc +0 -0
- package/py/src/g360_core/__pycache__/processor_segmentacion.cpython-314.pyc +0 -0
- package/py/src/g360_core/__pycache__/processor_sku.cpython-312.pyc +0 -0
- package/py/src/g360_core/__pycache__/processor_sku.cpython-314.pyc +0 -0
- package/py/src/g360_core/__pycache__/scanner.cpython-312.pyc +0 -0
- package/py/src/g360_core/__pycache__/scanner.cpython-314.pyc +0 -0
- package/py/src/g360_core/__pycache__/utils.cpython-312.pyc +0 -0
- package/py/src/g360_core/__pycache__/utils.cpython-314.pyc +0 -0
- package/src/assets/ingestion/core/__pycache__/ingestion.cpython-312.pyc +0 -0
- package/src/assets/signature/g360_flet/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/assets/signature/g360_flet/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/assets/signature/g360_flet/__pycache__/g360_signature.cpython-312.pyc +0 -0
- package/src/assets/signature/g360_flet/__pycache__/g360_signature.cpython-314.pyc +0 -0
- package/src/assets/templates/python-cli/package.json +0 -7
- /package/src/assets/config/{skills.json → agent-skills.json} +0 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""
|
|
2
|
+
G360 Dashboard - Template base con patrones pulidos.
|
|
3
|
+
|
|
4
|
+
Patrones disponibles:
|
|
5
|
+
- Sidebar colapsable con chips de filtro
|
|
6
|
+
- KPIs con glow backlight por color
|
|
7
|
+
- Buscador flotante con debounce
|
|
8
|
+
- Tablas paginadas con sort en headers
|
|
9
|
+
- Export a Excel con openpyxl
|
|
10
|
+
- Health filter por umbrales configurables
|
|
11
|
+
- Stale data detection
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import threading
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
|
|
18
|
+
import flet as ft
|
|
19
|
+
from src.config.theme import get_colors, rgba
|
|
20
|
+
from src.ui.kpi_card import KPICard
|
|
21
|
+
from src.ui.search_overlay import SearchOverlay
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Dashboard:
|
|
25
|
+
def __init__(self, page: ft.Page, theme_mode: str = "dark"):
|
|
26
|
+
self.page = page
|
|
27
|
+
self._theme_mode = theme_mode
|
|
28
|
+
self.c = get_colors(self._theme_mode)
|
|
29
|
+
self._search_timer = None
|
|
30
|
+
self._theme_button: ft.IconButton | None = None
|
|
31
|
+
|
|
32
|
+
# State
|
|
33
|
+
self._raw_data: dict | None = None
|
|
34
|
+
self._kpis: dict | None = None
|
|
35
|
+
self._cache_timestamp: str | None = None
|
|
36
|
+
self._stale_data: bool = False
|
|
37
|
+
|
|
38
|
+
# UI refs
|
|
39
|
+
self._kpi_row: ft.Row | None = None
|
|
40
|
+
self._main_content: ft.Container | None = None
|
|
41
|
+
self._sidebar: ft.Container | None = None
|
|
42
|
+
self._status_text = ft.Text(
|
|
43
|
+
"Cargue datos para comenzar",
|
|
44
|
+
size=13, color=self.c["text_muted"], weight=ft.FontWeight.W_500,
|
|
45
|
+
)
|
|
46
|
+
self._ts_text = ft.Text("", size=11, color=self.c["text_muted"], weight=ft.FontWeight.W_500)
|
|
47
|
+
self._stale_badge = ft.Container(
|
|
48
|
+
content=ft.Row([
|
|
49
|
+
ft.Icon(ft.Icons.WARNING_AMBER, size=14, color=self.c["warning"]),
|
|
50
|
+
ft.Text("Datos en caché", size=11, color=self.c["warning"], weight=ft.FontWeight.W_500),
|
|
51
|
+
], spacing=4, vertical_alignment=ft.CrossAxisAlignment.CENTER),
|
|
52
|
+
visible=False,
|
|
53
|
+
padding=ft.Padding(left=10, right=10, top=5, bottom=5),
|
|
54
|
+
bgcolor=rgba(self.c["warning"], 0.07),
|
|
55
|
+
border_radius=8,
|
|
56
|
+
)
|
|
57
|
+
self._empty_state = ft.Container(
|
|
58
|
+
visible=False,
|
|
59
|
+
content=ft.Column([
|
|
60
|
+
ft.Icon(ft.Icons.CLOUD_OFF, size=48, color=rgba(self.c["text_muted"], 0.35)),
|
|
61
|
+
ft.Text("Sin datos disponibles", size=16, color=self.c["text_muted"], weight=ft.FontWeight.W_600),
|
|
62
|
+
ft.Text("Verificando conexión...", size=13, color=rgba(self.c["text_muted"], 0.65)),
|
|
63
|
+
], horizontal_alignment=ft.CrossAxisAlignment.CENTER, spacing=12),
|
|
64
|
+
)
|
|
65
|
+
self._loading_overlay = ft.Container(
|
|
66
|
+
visible=False,
|
|
67
|
+
content=ft.Column([
|
|
68
|
+
ft.ProgressRing(width=40, height=40, color=self.c["accent"]),
|
|
69
|
+
ft.Container(height=12),
|
|
70
|
+
ft.Text("Cargando...", size=14, color=self.c["text_primary"]),
|
|
71
|
+
], horizontal_alignment=ft.CrossAxisAlignment.CENTER, spacing=0),
|
|
72
|
+
bgcolor=rgba(self.c["background"], 0.85),
|
|
73
|
+
expand=True,
|
|
74
|
+
alignment=ft.alignment.center,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Callbacks
|
|
78
|
+
self._on_theme_toggle = None
|
|
79
|
+
self._on_refresh = None
|
|
80
|
+
self._file_picker = ft.FilePicker()
|
|
81
|
+
|
|
82
|
+
def set_on_theme_toggle(self, cb):
|
|
83
|
+
self._on_theme_toggle = cb
|
|
84
|
+
|
|
85
|
+
def set_on_refresh(self, cb):
|
|
86
|
+
self._on_refresh = cb
|
|
87
|
+
|
|
88
|
+
def register_overlay(self):
|
|
89
|
+
self.page.overlay.append(self._file_picker)
|
|
90
|
+
|
|
91
|
+
def build(self) -> ft.Container:
|
|
92
|
+
self._sidebar = self._build_sidebar()
|
|
93
|
+
self._main_content = self._build_main_content()
|
|
94
|
+
|
|
95
|
+
return ft.Row(
|
|
96
|
+
controls=[
|
|
97
|
+
self._sidebar,
|
|
98
|
+
ft.VerticalDivider(width=1, color=self.c["border"]),
|
|
99
|
+
self._main_content,
|
|
100
|
+
],
|
|
101
|
+
expand=True,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def _build_header(self) -> ft.Container:
|
|
105
|
+
theme_icon = ft.Icons.DARK_MODE if self._theme_mode == "dark" else ft.Icons.LIGHT_MODE
|
|
106
|
+
self._theme_button = ft.IconButton(
|
|
107
|
+
icon=theme_icon,
|
|
108
|
+
icon_size=18,
|
|
109
|
+
tooltip="Cambiar tema",
|
|
110
|
+
on_click=lambda _: self._on_theme_toggle() if self._on_theme_toggle else None,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return ft.Container(
|
|
114
|
+
content=ft.Row([
|
|
115
|
+
ft.Text("G360 Dashboard", size=16, weight=ft.FontWeight.W_700, color=self.c["text_primary"]),
|
|
116
|
+
ft.Container(expand=True),
|
|
117
|
+
self._ts_text,
|
|
118
|
+
ft.Container(width=16),
|
|
119
|
+
self._stale_badge,
|
|
120
|
+
ft.Container(width=16),
|
|
121
|
+
self._status_text,
|
|
122
|
+
ft.Container(width=16),
|
|
123
|
+
self._theme_button,
|
|
124
|
+
], vertical_alignment=ft.CrossAxisAlignment.CENTER),
|
|
125
|
+
padding=ft.Padding(left=20, right=20, top=12, bottom=12),
|
|
126
|
+
bgcolor=self.c["surface"],
|
|
127
|
+
border_bottom=ft.BorderSide(1, self.c["border"]),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def _build_kpi_row(self, kpis: dict) -> ft.Row:
|
|
131
|
+
kpi_keys = ["almacenes", "skus", "disponible", "predespacho", "sin_catalogo", "alertas", "criticos", "alto_stock"]
|
|
132
|
+
kpi_colors = self.c.get("kpis", {})
|
|
133
|
+
|
|
134
|
+
controls = []
|
|
135
|
+
for i, key in enumerate(kpi_keys):
|
|
136
|
+
data = kpis.get(key, {})
|
|
137
|
+
card = KPICard(
|
|
138
|
+
label=key.replace("_", " ").title(),
|
|
139
|
+
value=str(data.get("value", "-")),
|
|
140
|
+
color=kpi_colors.get(key, list(kpi_colors.values())[0]),
|
|
141
|
+
subtext=data.get("subtext", ""),
|
|
142
|
+
)
|
|
143
|
+
controls.append(card)
|
|
144
|
+
if i < len(kpi_keys) - 1:
|
|
145
|
+
controls.append(ft.Container(width=8))
|
|
146
|
+
|
|
147
|
+
return ft.Row(
|
|
148
|
+
controls=controls,
|
|
149
|
+
wrap=True,
|
|
150
|
+
spacing=0,
|
|
151
|
+
run_spacing=8,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def _build_sidebar(self) -> ft.Container:
|
|
155
|
+
search_field = ft.TextField(
|
|
156
|
+
hint_text="Buscar SKU, descripción...",
|
|
157
|
+
border_radius=8,
|
|
158
|
+
height=36,
|
|
159
|
+
text_size=12,
|
|
160
|
+
dense=True,
|
|
161
|
+
border=ft.InputBorder.OUTLINE,
|
|
162
|
+
border_color=self.c["border"],
|
|
163
|
+
focused_border_color=self.c["accent"],
|
|
164
|
+
cursor_color=self.c["accent"],
|
|
165
|
+
hint_style=ft.TextStyle(size=11, color=self.c["text_muted"]),
|
|
166
|
+
suffix_icon=ft.Icon(ft.Icons.KEYBOARD_RETURN, size=14, color=rgba(self.c["text_muted"], 0.5)),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return ft.Container(
|
|
170
|
+
content=ft.Column([
|
|
171
|
+
ft.Container(height=16),
|
|
172
|
+
ft.Text("Filtros", size=12, weight=ft.FontWeight.W_600, color=self.c["text_muted"]),
|
|
173
|
+
ft.Container(height=8),
|
|
174
|
+
search_field,
|
|
175
|
+
ft.Container(height=16),
|
|
176
|
+
ft.Divider(color=self.c["border"]),
|
|
177
|
+
ft.Container(height=8),
|
|
178
|
+
ft.Text("Almacenes", size=12, weight=ft.FontWeight.W_600, color=self.c["text_muted"]),
|
|
179
|
+
ft.Container(height=8),
|
|
180
|
+
ft.Column([], scroll=ft.ScrollMode.AUTO, expand=True, id="sidebar_alms"),
|
|
181
|
+
], spacing=0, expand=True),
|
|
182
|
+
width=220,
|
|
183
|
+
bgcolor=self.c["surface"],
|
|
184
|
+
padding=ft.Padding(left=12, right=12, top=16, bottom=12),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def _build_main_content(self) -> ft.Container:
|
|
188
|
+
return ft.Container(
|
|
189
|
+
content=ft.Column([
|
|
190
|
+
self._build_header(),
|
|
191
|
+
ft.Container(content=self._kpi_row if self._kpi_row else self._build_kpi_row({}), expand=False),
|
|
192
|
+
ft.Container(height=12),
|
|
193
|
+
ft.Container(
|
|
194
|
+
content=ft.Column([], scroll=ft.ScrollMode.AUTO, expand=True, id="main_cards"),
|
|
195
|
+
expand=True,
|
|
196
|
+
),
|
|
197
|
+
self._empty_state,
|
|
198
|
+
self._loading_overlay,
|
|
199
|
+
], spacing=0, expand=True),
|
|
200
|
+
expand=True,
|
|
201
|
+
bgcolor=self.c["background"],
|
|
202
|
+
padding=ft.Padding(left=16, right=16, top=16, bottom=16),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# --- Update methods ---
|
|
206
|
+
|
|
207
|
+
def update_data(self, raw_data: dict, cache_timestamp: str | None = None, api_timestamp: str | None = None, stale: bool = False):
|
|
208
|
+
self._raw_data = raw_data
|
|
209
|
+
self._cache_timestamp = cache_timestamp
|
|
210
|
+
self._stale_data = stale
|
|
211
|
+
# Subclases implementan la logica de renderizado
|
|
212
|
+
self._render()
|
|
213
|
+
|
|
214
|
+
def _render(self):
|
|
215
|
+
# Implementar en subclase segun el dominio
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
def update_theme(self, mode: str):
|
|
219
|
+
self._theme_mode = mode
|
|
220
|
+
self.c = get_colors(mode)
|
|
221
|
+
# Reconstruir UI con nuevos colores
|
|
222
|
+
self.page.bgcolor = self.c["background"]
|
|
223
|
+
self._rebuild_ui()
|
|
224
|
+
|
|
225
|
+
def _rebuild_ui(self):
|
|
226
|
+
if self._main_content:
|
|
227
|
+
self._main_content.content = self._build_main_content().content
|
|
228
|
+
self.page.update()
|
|
229
|
+
|
|
230
|
+
def set_loading(self, loading: bool, message: str = "Cargando..."):
|
|
231
|
+
self._loading_overlay.visible = loading
|
|
232
|
+
if loading:
|
|
233
|
+
self._loading_overlay.content.controls[2].value = message
|
|
234
|
+
self.page.update()
|
|
235
|
+
|
|
236
|
+
def set_offline(self, offline: bool):
|
|
237
|
+
if offline:
|
|
238
|
+
self._status_text.value = "Sin conexión"
|
|
239
|
+
self._status_text.color = self.c["error"]
|
|
240
|
+
else:
|
|
241
|
+
self._status_text.value = "Conectado"
|
|
242
|
+
self._status_text.color = self.c["success"]
|
|
243
|
+
self.page.update()
|
|
244
|
+
|
|
245
|
+
def _show_snack(self, msg: str, is_error: bool = False):
|
|
246
|
+
self.page.snack_bar = ft.SnackBar(
|
|
247
|
+
content=ft.Text(msg, size=13),
|
|
248
|
+
bgcolor=self.c["error"] if is_error else self.c["accent"],
|
|
249
|
+
duration=3000,
|
|
250
|
+
)
|
|
251
|
+
self.page.open(self.page.snack_bar)
|
|
252
|
+
|
|
253
|
+
def _show_empty_state(self, title: str, subtitle: str = ""):
|
|
254
|
+
self._empty_state.content.controls[1].value = title
|
|
255
|
+
if subtitle:
|
|
256
|
+
if len(self._empty_state.content.controls) > 2:
|
|
257
|
+
self._empty_state.content.controls[2].value = subtitle
|
|
258
|
+
else:
|
|
259
|
+
self._empty_state.content.controls.append(
|
|
260
|
+
ft.Text(subtitle, size=13, color=rgba(self.c["text_muted"], 0.65))
|
|
261
|
+
)
|
|
262
|
+
self._empty_state.visible = True
|
|
263
|
+
self.page.update()
|
|
264
|
+
|
|
265
|
+
def _hide_empty_state(self):
|
|
266
|
+
self._empty_state.visible = False
|
|
267
|
+
self.page.update()
|
|
268
|
+
|
|
269
|
+
def _set_empty_state_status(self, msg: str, color: str):
|
|
270
|
+
for ctrl in self._empty_state.content.controls:
|
|
271
|
+
if isinstance(ctrl, ft.Text) and ctrl.size == 13:
|
|
272
|
+
ctrl.value = msg
|
|
273
|
+
ctrl.color = color
|
|
274
|
+
break
|
|
275
|
+
self.page.update()
|
|
276
|
+
|
|
277
|
+
def _update_refresh_status(self, cache_ts: str | None):
|
|
278
|
+
if cache_ts:
|
|
279
|
+
try:
|
|
280
|
+
dt = datetime.fromisoformat(cache_ts)
|
|
281
|
+
age_min = (datetime.now() - dt).total_seconds() / 60
|
|
282
|
+
self._ts_text.value = f"Hace {int(age_min)} min"
|
|
283
|
+
except Exception:
|
|
284
|
+
self._ts_text.value = ""
|
|
285
|
+
else:
|
|
286
|
+
self._ts_text.value = ""
|
|
287
|
+
|
|
288
|
+
def format_cache_timestamp(self, ts: str | None) -> str:
|
|
289
|
+
if not ts:
|
|
290
|
+
return ""
|
|
291
|
+
try:
|
|
292
|
+
return datetime.fromisoformat(ts).strftime("%H:%M:%S")
|
|
293
|
+
except Exception:
|
|
294
|
+
return ts[:19]
|
|
295
|
+
|
|
296
|
+
def _show_stale_warning(self):
|
|
297
|
+
self._stale_badge.visible = True
|
|
298
|
+
self.page.update()
|
|
299
|
+
|
|
300
|
+
def _hide_stale_warning(self):
|
|
301
|
+
self._stale_badge.visible = False
|
|
302
|
+
self.page.update()
|
|
303
|
+
|
|
304
|
+
def show_search_overlay(self):
|
|
305
|
+
"""Abre el buscador flotante."""
|
|
306
|
+
overlay = SearchOverlay(self.page, self.c)
|
|
307
|
+
self.page.overlay.append(overlay)
|
|
308
|
+
self.page.open(overlay)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
G360 KPI Card - Componente reutilizable para indicadores clave.
|
|
3
|
+
|
|
4
|
+
Patron: Card con glow backlight por color, valor numerico grande,
|
|
5
|
+
subtexto opcional. Clickable para abrir dialogos detallados.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import flet as ft
|
|
10
|
+
from src.config.theme import rgba
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class KPICard(ft.Container):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
label: str,
|
|
17
|
+
value: str,
|
|
18
|
+
color: str,
|
|
19
|
+
subtext: str = "",
|
|
20
|
+
on_click=None,
|
|
21
|
+
):
|
|
22
|
+
super().__init__()
|
|
23
|
+
self._label = label
|
|
24
|
+
self._value = value
|
|
25
|
+
self._color = color
|
|
26
|
+
self._subtext = subtext
|
|
27
|
+
self._on_click = on_click
|
|
28
|
+
|
|
29
|
+
self.content = ft.Column(
|
|
30
|
+
controls=[
|
|
31
|
+
ft.Text(label, size=11, color=rgba(color, 0.8), weight=ft.FontWeight.W_500),
|
|
32
|
+
ft.Container(height=4),
|
|
33
|
+
ft.Text(
|
|
34
|
+
value,
|
|
35
|
+
size=22,
|
|
36
|
+
color=color,
|
|
37
|
+
weight=ft.FontWeight.W_700,
|
|
38
|
+
font_family="JetBrains Mono",
|
|
39
|
+
),
|
|
40
|
+
ft.Container(height=2),
|
|
41
|
+
ft.Text(subtext, size=10, color=rgba(color, 0.6)),
|
|
42
|
+
],
|
|
43
|
+
spacing=0,
|
|
44
|
+
horizontal_alignment=ft.CrossAxisAlignment.START,
|
|
45
|
+
)
|
|
46
|
+
self.padding = ft.Padding(left=14, right=14, top=10, bottom=10)
|
|
47
|
+
self.bgcolor = rgba(color, 0.08)
|
|
48
|
+
self.border_radius = 10
|
|
49
|
+
self.shadow = ft.BoxShadow(
|
|
50
|
+
spread_radius=0.5,
|
|
51
|
+
blur_radius=12,
|
|
52
|
+
color=rgba(color, 0.15),
|
|
53
|
+
offset=ft.Offset(0, 2),
|
|
54
|
+
)
|
|
55
|
+
self.border = ft.Border(bottom=ft.BorderSide(2, rgba(color, 0.4)))
|
|
56
|
+
self.cursor_type = ft.CursorType.CLICK if on_click else ft.CursorType.NONE
|
|
57
|
+
|
|
58
|
+
if on_click:
|
|
59
|
+
self.on_click = on_click
|
|
60
|
+
else:
|
|
61
|
+
self.on_click = None
|
|
62
|
+
|
|
63
|
+
def update_value(self, value: str, subtext: str = ""):
|
|
64
|
+
self._value = value
|
|
65
|
+
self._subtext = subtext
|
|
66
|
+
self.content.controls[2].value = value
|
|
67
|
+
if len(self.content.controls) > 3:
|
|
68
|
+
self.content.controls[3].value = subtext
|
|
69
|
+
self.update()
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
G360 Search Overlay - Buscador flotante con debounce.
|
|
3
|
+
|
|
4
|
+
Busqueda contextual en tiempo real con resultados en vivo.
|
|
5
|
+
Enter abre el primer resultado, Escape o click fuera cierra.
|
|
6
|
+
Auto-limpieza al enfocar con texto previo.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
|
|
12
|
+
import flet as ft
|
|
13
|
+
from src.config.theme import rgba
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SearchOverlay(ft.Container):
|
|
17
|
+
def __init__(self, page: ft.Page, colors: dict):
|
|
18
|
+
super().__init__()
|
|
19
|
+
self.page = page
|
|
20
|
+
self.c = colors
|
|
21
|
+
self._results: list[dict] = []
|
|
22
|
+
self._debounce_task = None
|
|
23
|
+
self._selected_index = 0
|
|
24
|
+
|
|
25
|
+
self.field = ft.TextField(
|
|
26
|
+
hint_text="Buscar SKU, descripción, línea...",
|
|
27
|
+
border_radius=10,
|
|
28
|
+
height=44,
|
|
29
|
+
text_size=13,
|
|
30
|
+
dense=True,
|
|
31
|
+
border=ft.InputBorder.OUTLINE,
|
|
32
|
+
border_color=self.c["border"],
|
|
33
|
+
focused_border_color=self.c["accent"],
|
|
34
|
+
cursor_color=self.c["accent"],
|
|
35
|
+
hint_style=ft.TextStyle(size=12, color=self.c["text_muted"]),
|
|
36
|
+
autofocus=True,
|
|
37
|
+
on_change=self._on_change,
|
|
38
|
+
on_submit=self._on_submit,
|
|
39
|
+
on_focus=self._on_focus,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
self.content = ft.Column(
|
|
43
|
+
controls=[
|
|
44
|
+
self.field,
|
|
45
|
+
ft.Container(
|
|
46
|
+
content=ft.Column([], scroll=ft.ScrollMode.AUTO, max_height=300, id="search_results"),
|
|
47
|
+
bgcolor=self.c["surface"],
|
|
48
|
+
border_radius=ft.BorderRadius.only(bottom_left=10, bottom_right=10),
|
|
49
|
+
border=ft.border.Top(border_color=self.c["border"], border_style=ft.BorderSide(1)),
|
|
50
|
+
visible=False,
|
|
51
|
+
padding=ft.Padding(left=0, right=0, top=0, bottom=0),
|
|
52
|
+
max_height=300,
|
|
53
|
+
height=300,
|
|
54
|
+
),
|
|
55
|
+
],
|
|
56
|
+
spacing=0,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
self.bgcolor = self.c["surface"]
|
|
60
|
+
self.border_radius = 10
|
|
61
|
+
self.padding = ft.Padding(left=16, right=16, top=12, bottom=12)
|
|
62
|
+
self.width = 480
|
|
63
|
+
self.alignment = ft.alignment.top_center
|
|
64
|
+
self.margin = ft.margin.only(top=60)
|
|
65
|
+
|
|
66
|
+
def _on_focus(self, e):
|
|
67
|
+
if self.field.value:
|
|
68
|
+
self.field.value = ""
|
|
69
|
+
self.field.update()
|
|
70
|
+
|
|
71
|
+
async def _on_change(self, e):
|
|
72
|
+
query = (e.control.value or "").strip().lower()
|
|
73
|
+
if self._debounce_task:
|
|
74
|
+
self._debounce_task.cancel()
|
|
75
|
+
if len(query) < 2:
|
|
76
|
+
self._hide_results()
|
|
77
|
+
return
|
|
78
|
+
self._debounce_task = asyncio.create_task(self._debounce_search(query))
|
|
79
|
+
|
|
80
|
+
async def _debounce_search(self, query: str):
|
|
81
|
+
await asyncio.sleep(0.25)
|
|
82
|
+
self._results = await self._do_search(query)
|
|
83
|
+
self._show_results(self._results)
|
|
84
|
+
|
|
85
|
+
async def _do_search(self, query: str) -> list[dict]:
|
|
86
|
+
"""Implementar en subclase. Retorna lista de dicts {sku, desc, ...}."""
|
|
87
|
+
return []
|
|
88
|
+
|
|
89
|
+
def _show_results(self, results: list[dict]):
|
|
90
|
+
if not results:
|
|
91
|
+
self._hide_results()
|
|
92
|
+
return
|
|
93
|
+
controls = []
|
|
94
|
+
for i, item in enumerate(results[:10]):
|
|
95
|
+
is_selected = i == self._selected_index
|
|
96
|
+
controls.append(
|
|
97
|
+
ft.Container(
|
|
98
|
+
content=ft.Row([
|
|
99
|
+
ft.Text(item.get("sku", ""), size=12, color=self.c["accent"], weight=ft.FontWeight.W_600),
|
|
100
|
+
ft.Container(width=8),
|
|
101
|
+
ft.Text(item.get("desc", item.get("descripcion", "")), size=12, color=self.c["text_primary"]),
|
|
102
|
+
], spacing=0),
|
|
103
|
+
bgcolor=rgba(self.c["accent"], 0.1) if is_selected else "transparent",
|
|
104
|
+
border_radius=6,
|
|
105
|
+
padding=ft.Padding(left=12, right=12, top=8, bottom=8),
|
|
106
|
+
on_click=lambda _, r=item: self._on_select(r),
|
|
107
|
+
mouse_cursor=ft.CursorType.CLICK,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
results_col = self.content.controls[1]
|
|
111
|
+
results_col.content.controls = controls
|
|
112
|
+
results_col.visible = True
|
|
113
|
+
self.update()
|
|
114
|
+
|
|
115
|
+
def _hide_results(self):
|
|
116
|
+
results_col = self.content.controls[1]
|
|
117
|
+
results_col.content.controls = []
|
|
118
|
+
results_col.visible = False
|
|
119
|
+
self.update()
|
|
120
|
+
|
|
121
|
+
def _on_submit(self, e):
|
|
122
|
+
if self._results:
|
|
123
|
+
self._on_select(self._results[self._selected_index])
|
|
124
|
+
|
|
125
|
+
def _on_select(self, item: dict):
|
|
126
|
+
self._hide_results()
|
|
127
|
+
self.page.close(self)
|
|
128
|
+
|
|
129
|
+
def did_mount(self):
|
|
130
|
+
"""Captura Escape para cerrar."""
|
|
131
|
+
def on_keyboard(e: ft.KeyboardEvent):
|
|
132
|
+
if e.key == "Escape":
|
|
133
|
+
self.page.close(self)
|
|
134
|
+
elif e.key == "ArrowDown":
|
|
135
|
+
self._selected_index = min(self._selected_index + 1, len(self._results) - 1)
|
|
136
|
+
self._show_results(self._results)
|
|
137
|
+
elif e.key == "ArrowUp":
|
|
138
|
+
self._selected_index = max(self._selected_index - 1, 0)
|
|
139
|
+
self._show_results(self._results)
|
|
140
|
+
elif e.key == "Enter" and self._results:
|
|
141
|
+
self._on_select(self._results[self._selected_index])
|
|
142
|
+
|
|
143
|
+
self.page.on_keyboard_event = on_keyboard
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def sync_dir(src, dst):
|
|
7
|
+
if not os.path.exists(src):
|
|
8
|
+
return
|
|
9
|
+
if not os.path.exists(dst):
|
|
10
|
+
os.makedirs(dst)
|
|
11
|
+
for item in os.listdir(dst):
|
|
12
|
+
dst_item = os.path.join(dst, item)
|
|
13
|
+
src_item = os.path.join(src, item)
|
|
14
|
+
if not os.path.exists(src_item):
|
|
15
|
+
if os.path.isdir(dst_item):
|
|
16
|
+
shutil.rmtree(dst_item)
|
|
17
|
+
print(f"Eliminado directorio obsoleto: {dst_item}")
|
|
18
|
+
else:
|
|
19
|
+
os.remove(dst_item)
|
|
20
|
+
print(f"Eliminado archivo obsoleto: {dst_item}")
|
|
21
|
+
for item in os.listdir(src):
|
|
22
|
+
src_item = os.path.join(src, item)
|
|
23
|
+
dst_item = os.path.join(dst, item)
|
|
24
|
+
if os.path.isdir(src_item):
|
|
25
|
+
if item == "__pycache__":
|
|
26
|
+
continue
|
|
27
|
+
sync_dir(src_item, dst_item)
|
|
28
|
+
else:
|
|
29
|
+
if (not os.path.exists(dst_item) or
|
|
30
|
+
os.stat(src_item).st_mtime != os.stat(dst_item).st_mtime or
|
|
31
|
+
os.stat(src_item).st_size != os.stat(dst_item).st_size):
|
|
32
|
+
shutil.copy2(src_item, dst_item)
|
|
33
|
+
print(f"Sincronizado: {src_item} -> {dst_item}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main():
|
|
37
|
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
|
38
|
+
portable_dir = os.path.join(base_dir, "g360-app-polished-portable")
|
|
39
|
+
|
|
40
|
+
if not os.path.exists(portable_dir):
|
|
41
|
+
print(f"Error: No se encontro la carpeta portable '{portable_dir}'")
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
print("Iniciando sincronizacion con la version portable...")
|
|
45
|
+
|
|
46
|
+
files_to_sync = [
|
|
47
|
+
"main.py",
|
|
48
|
+
"run.bat", "launch.vbs", "launch_minimized.bat", "create_shortcut.vbs",
|
|
49
|
+
"build-portable.bat", "README.md",
|
|
50
|
+
]
|
|
51
|
+
for filename in files_to_sync:
|
|
52
|
+
src_file = os.path.join(base_dir, filename)
|
|
53
|
+
dst_file = os.path.join(portable_dir, filename)
|
|
54
|
+
if os.path.exists(src_file):
|
|
55
|
+
shutil.copy2(src_file, dst_file)
|
|
56
|
+
print(f"Copiado: {filename} -> portable/{filename}")
|
|
57
|
+
else:
|
|
58
|
+
print(f"Omitido (no existe en origen): {filename}")
|
|
59
|
+
|
|
60
|
+
sync_dir(os.path.join(base_dir, "src"), os.path.join(portable_dir, "src"))
|
|
61
|
+
sync_dir(os.path.join(base_dir, "assets"), os.path.join(portable_dir, "assets"))
|
|
62
|
+
sync_dir(os.path.join(base_dir, "g360_flet"), os.path.join(portable_dir, "g360_flet"))
|
|
63
|
+
|
|
64
|
+
for pyc in Path(portable_dir).rglob("__pycache__"):
|
|
65
|
+
shutil.rmtree(pyc)
|
|
66
|
+
|
|
67
|
+
print("\nSincronizacion finalizada con exito!")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
main()
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @version 1.0.0
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
export default function App() {
|
|
8
|
+
export default function App(props) {
|
|
9
9
|
const titulo = 'Mi Proyecto G360';
|
|
10
10
|
|
|
11
11
|
return (
|
|
@@ -14,7 +14,7 @@ export default function App() {
|
|
|
14
14
|
<h1>{titulo}</h1>
|
|
15
15
|
</header>
|
|
16
16
|
<main>
|
|
17
|
-
|
|
17
|
+
{props.children}
|
|
18
18
|
</main>
|
|
19
19
|
</div>
|
|
20
20
|
);
|
package/src/cli.js
CHANGED
|
@@ -33,7 +33,7 @@ function configAction(options) {
|
|
|
33
33
|
console.log(chalk.gray(' positions: bottom-right, bottom-left, bottom-center, footer-right, footer-left'));
|
|
34
34
|
console.log(chalk.white(' templates:'));
|
|
35
35
|
console.log(chalk.gray(' web-pwa, lit-web, solid-web, svelte-web,'));
|
|
36
|
-
console.log(chalk.gray(' python-cli, python-flet, python-flet-migrate, python-customtkinter, vba-excel'));
|
|
36
|
+
console.log(chalk.gray(' python-cli, python-flet-polished (default), python-flet-legacy, python-flet-migrate, python-customtkinter, vba-excel'));
|
|
37
37
|
console.log(chalk.white(' skills:'));
|
|
38
38
|
console.log(chalk.gray(' corporativo, corporativo-movil, corporativo-g360, corporativo-g360-movil,'));
|
|
39
39
|
console.log(chalk.gray(' moderno, moderno-movil, minimalista, custom,'));
|
|
@@ -72,8 +72,8 @@ program
|
|
|
72
72
|
program
|
|
73
73
|
.command('init')
|
|
74
74
|
.argument('<name>', 'Project name')
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
.option('-t, --template <type>', 'Project template type (default: auto segun skill — python-flet-polished para Flet)', 'auto')
|
|
76
|
+
.option('-s, --skill <skill>', 'Skill to use (corporativo, corporativo-movil, moderno, moderno-movil, minimalista, custom, flet-desktop, flet-desktop-corporativo, flet-desktop-polished, cipsa, cipsa-movil)', 'corporativo-movil')
|
|
77
77
|
.option('-d, --dir <path>', 'Target directory', '.')
|
|
78
78
|
.option('--dry-run', 'Preview without creating files')
|
|
79
79
|
.option('--force', 'Overwrite existing files')
|
|
@@ -143,7 +143,7 @@ program
|
|
|
143
143
|
program
|
|
144
144
|
.command('convert')
|
|
145
145
|
.argument('[path]', 'Path to convert', '.')
|
|
146
|
-
|
|
146
|
+
.option('-s, --skill <skill>', 'Skill to apply (corporativo, corporativo-movil, moderno, moderno-movil, minimalista, custom, flet-desktop, flet-desktop-corporativo, flet-desktop-polished, cipsa, cipsa-movil)', 'corporativo-movil')
|
|
147
147
|
.option('--dry-run', 'Preview without applying')
|
|
148
148
|
.option('--restructure', 'Restructure project files')
|
|
149
149
|
.option('--force', 'Force dangerous changes')
|
package/src/commands/clean.js
CHANGED
|
@@ -7,11 +7,8 @@
|
|
|
7
7
|
import chalk from 'chalk';
|
|
8
8
|
import fs from 'fs-extra';
|
|
9
9
|
import path from 'path';
|
|
10
|
-
import { fileURLToPath } from 'url';
|
|
11
10
|
import { getAllFiles } from '../lib/file-utils.js';
|
|
12
11
|
|
|
13
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
-
|
|
15
12
|
const DEAD_PATTERNS = [
|
|
16
13
|
'backup', 'backup-old', 'backups',
|
|
17
14
|
'temp', 'tmp', '.tmp',
|
|
@@ -292,15 +289,6 @@ async function findOrphanFiles(projectDir, allFiles) {
|
|
|
292
289
|
|
|
293
290
|
function analyzeFileOrganization(files) {
|
|
294
291
|
const misplaced = [];
|
|
295
|
-
const structureRules = {
|
|
296
|
-
'src/components': ['.js', '.jsx', '.ts', '.tsx', '.svelte', '.vue'],
|
|
297
|
-
'src/pages': ['.js', '.jsx', '.ts', '.tsx', '.svelte', '.vue'],
|
|
298
|
-
'src/utils': ['.js', '.ts'],
|
|
299
|
-
'src/hooks': ['.js', '.ts'],
|
|
300
|
-
'src/services': ['.js', '.ts'],
|
|
301
|
-
'src/styles': ['.css', '.scss', '.less'],
|
|
302
|
-
'src/assets': ['.png', '.jpg', '.svg', '.gif', '.webp']
|
|
303
|
-
};
|
|
304
292
|
|
|
305
293
|
const components = files.filter(f => {
|
|
306
294
|
const name = path.basename(f);
|