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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user