chore: estructura inicial del MVP

Backend (Express + MariaDB) y frontend (React + Vite, identidad Fluent/Windows) con el
nucleo funcional: seguridad, organizacion, proyectos, actividades, subtareas, dependencias,
dashboard, calendario, gantt, comentarios, adjuntos, notificaciones in-app y catalogos.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-09-03 08:37:58 -05:00
commit 1d22adc42e
135 changed files with 10913 additions and 0 deletions
@@ -0,0 +1,127 @@
import { useState } from 'react';
import { Plus, Save, Trash2 } from 'lucide-react';
import { useUi } from '../../context/ui-context.jsx';
import { useAsync, useAction } from '../../hooks/useAsync.js';
import { listCatalog, createCatalogItem, updateCatalogItem, deleteCatalogItem } from '../../services/catalogs.js';
import { ListView } from '../../components/ListView.jsx';
import { Button } from '../../components/Button.jsx';
import { Field, FieldRow } from '../../components/Field.jsx';
import { ComboBox } from '../../components/ComboBox.jsx';
import { Badge } from '../../components/Badge.jsx';
const COLOR_OPTIONS = ['accent', 'success', 'warning', 'danger', 'muted'].map((c) => ({ value: c, label: c }));
function emptyForm(fields) {
const form = {};
for (const f of fields) form[f.key] = f.type === 'checkbox' ? false : '';
return form;
}
// Editor generico reutilizable para los 4 catalogos (prioridades, estados, tipos de proyecto).
export function CatalogEditor({ catalogKey, fields, title }) {
const { t, openDialog, pushToast } = useUi();
const runAction = useAction();
const { data: items, loading, reload } = useAsync(() => listCatalog(catalogKey), [catalogKey]);
const [editing, setEditing] = useState(false);
const [selected, setSelected] = useState(null);
const [form, setForm] = useState(emptyForm(fields));
function startNew() {
setSelected(null);
setForm(emptyForm(fields));
setEditing(true);
}
function startEdit(item) {
setSelected(item);
const next = {};
for (const f of fields) next[f.key] = item[f.key];
setForm(next);
setEditing(true);
}
async function save() {
await runAction(async () => {
if (selected) await updateCatalogItem(catalogKey, selected.id, form);
else await createCatalogItem(catalogKey, form);
await reload();
setEditing(false);
pushToast({ tone: 'success', message: t('common_saved') });
});
}
function confirmDelete(item) {
openDialog({
tone: 'warning',
title: t('common_confirm_delete_title'),
message: t('common_confirm_delete_msg'),
actions: [
{ label: t('common_cancel'), variant: 'standard' },
{ label: t('common_delete'), variant: 'danger', onClick: () => runAction(async () => {
await deleteCatalogItem(catalogKey, item.id);
await reload();
setEditing(false);
}) },
],
});
}
const columns = fields.map((f) => ({
key: f.key,
label: f.label,
render: f.type === 'color' ? (row) => <Badge color={row[f.key]}>{row[f.key]}</Badge>
: f.type === 'checkbox' ? (row) => (row[f.key] ? t('common_yes') : t('common_no'))
: undefined,
}));
if (loading || !items) return <div className="text-muted">{t('common_loading')}</div>;
return (
<div>
<div className="toolbar">
<h4 style={{ margin: 0 }}>{title}</h4>
<Button size="sm" variant="accent" icon={Plus} onClick={startNew}>{t('common_new')}</Button>
</div>
<ListView
columns={columns}
rows={items}
getRowKey={(r) => r.id}
onRowClick={startEdit}
rowActions={(r) => [
{ label: t('common_edit'), onClick: () => startEdit(r) },
{ label: t('common_delete'), icon: Trash2, danger: true, onClick: () => confirmDelete(r) },
]}
/>
{editing && (
<div className="panel-card" style={{ marginTop: 12, maxWidth: 480 }}>
<FieldRow>
{fields.map((f) => (
<Field key={f.key} label={f.label}>
{f.type === 'color' ? (
<ComboBox options={COLOR_OPTIONS} value={form[f.key]} onChange={(v) => setForm((s) => ({ ...s, [f.key]: v }))} />
) : f.type === 'checkbox' ? (
<label className="checkbox-row">
<input type="checkbox" checked={!!form[f.key]} onChange={(e) => setForm((s) => ({ ...s, [f.key]: e.target.checked }))} />
{f.label}
</label>
) : (
<input
className="input"
type={f.type === 'number' ? 'number' : 'text'}
value={form[f.key]}
onChange={(e) => setForm((s) => ({ ...s, [f.key]: f.type === 'number' ? Number(e.target.value) : e.target.value }))}
/>
)}
</Field>
))}
</FieldRow>
<div style={{ display: 'flex', gap: 8 }}>
<Button size="sm" variant="accent" icon={Save} onClick={save}>{t('common_save')}</Button>
<Button size="sm" variant="standard" onClick={() => setEditing(false)}>{t('common_cancel')}</Button>
</div>
</div>
)}
</div>
);
}
@@ -0,0 +1,89 @@
import { Palette, ListChecks, Flag, FolderKanban, Layers } from 'lucide-react';
import { useUi } from '../../context/ui-context.jsx';
import { Expander } from '../../components/Expander.jsx';
import { Field, FieldRow } from '../../components/Field.jsx';
import { ComboBox } from '../../components/ComboBox.jsx';
import { CatalogEditor } from './CatalogEditor.jsx';
export default function ConfiguracionPage() {
const { t, theme, setTheme, lang, setLang, buttonMode, setButtonMode } = useUi();
return (
<div>
<h2 className="page-title mt-0" style={{ marginBottom: 16 }}>{t('config_title')}</h2>
<Expander icon={Palette} title={t('config_appearance')} defaultOpen>
<FieldRow>
<Field label="Tema / Theme">
<ComboBox
options={[{ value: 'dark', label: t('theme_dark') }, { value: 'light', label: t('theme_light') }]}
value={theme} onChange={setTheme}
/>
</Field>
<Field label="Idioma / Language">
<ComboBox
options={[{ value: 'es-EC', label: t('lang_es') }, { value: 'en-US', label: t('lang_en') }]}
value={lang} onChange={setLang}
/>
</Field>
<Field label="Botones / Buttons">
<ComboBox
options={[
{ value: 'icon-text', label: t('button_mode_icon_text') },
{ value: 'icon', label: t('button_mode_icon') },
{ value: 'text', label: t('button_mode_text') },
]}
value={buttonMode} onChange={setButtonMode}
/>
</Field>
</FieldRow>
</Expander>
<Expander icon={Flag} title={t('config_priorities')}>
<CatalogEditor
catalogKey="priorities"
title={t('config_priorities')}
fields={[
{ key: 'name', label: t('common_name'), type: 'text' },
{ key: 'level', label: '#', type: 'number' },
{ key: 'color', label: 'Color', type: 'color' },
]}
/>
</Expander>
<Expander icon={ListChecks} title={t('config_project_statuses')}>
<CatalogEditor
catalogKey="project-statuses"
title={t('config_project_statuses')}
fields={[
{ key: 'name', label: t('common_name'), type: 'text' },
{ key: 'sort_order', label: '#', type: 'number' },
{ key: 'color', label: 'Color', type: 'color' },
{ key: 'is_closed', label: 'Cierra', type: 'checkbox' },
]}
/>
</Expander>
<Expander icon={Layers} title={t('config_activity_statuses')}>
<CatalogEditor
catalogKey="activity-statuses"
title={t('config_activity_statuses')}
fields={[
{ key: 'name', label: t('common_name'), type: 'text' },
{ key: 'sort_order', label: '#', type: 'number' },
{ key: 'color', label: 'Color', type: 'color' },
{ key: 'is_closed', label: 'Cierra', type: 'checkbox' },
]}
/>
</Expander>
<Expander icon={FolderKanban} title={t('config_project_types')}>
<CatalogEditor
catalogKey="project-types"
title={t('config_project_types')}
fields={[{ key: 'name', label: t('common_name'), type: 'text' }]}
/>
</Expander>
</div>
);
}