瀏覽代碼

Add base app

Vsevolod Levitan 3 天之前
父節點
當前提交
757e6854e2

+ 4 - 1
src/App.svelte

@@ -1,7 +1,10 @@
 <script>
+  import Wizard from "./lib/components/Wizard.svelte";
 </script>
 
-<main></main>
+<main>
+  <Wizard />
+</main>
 
 <style>
 </style>

+ 49 - 0
src/lib/components/ProgressBar.svelte

@@ -0,0 +1,49 @@
+<script>
+    export let step;
+    export let total;
+</script>
+
+<div class="progress-container">
+    <div class="progress-bar" style:width={`${(step / total) * 100}%`}></div>
+</div>
+<div class="steps">
+    {#each Array(total) as _, i}
+        <span
+            class="step-dot"
+            class:active={step > i + 1}
+            class:current={step === i + 1}
+        ></span>
+    {/each}
+</div>
+
+<style>
+    .progress-container {
+        height: 8px;
+        background: #eee;
+        border-radius: 4px;
+        overflow: hidden;
+        margin-bottom: 2rem;
+    }
+    .progress-bar {
+        height: 100%;
+        background: #2563eb;
+        transition: width 0.3s ease;
+    }
+    .steps {
+        display: flex;
+        justify-content: space-between;
+    }
+    .step-dot {
+        width: 12px;
+        height: 12px;
+        background: #ddd;
+        border-radius: 50%;
+    }
+    .step-dot.active {
+        background: #2563eb;
+    }
+    .step-dot.current {
+        background: #1d4ed8;
+        transform: scale(1.2);
+    }
+</style>

+ 71 - 0
src/lib/components/ResultCard.svelte

@@ -0,0 +1,71 @@
+<script>
+    import { fade } from "svelte/transition";
+
+    let { result, reset } = $props();
+</script>
+
+<div class="result-card" transition:fade>
+    <h2>Рекомендуем: <strong>{result.recommendation}</strong></h2>
+    <p>{result.description}</p>
+
+    <div class="section">
+        <h4>Налоговый режим:</h4>
+        <ul>
+            {#each result.taxes as tax}
+                <li>
+                    <strong>{tax.name}</strong>: {tax.rate} →
+                    <em>{tax.note}</em>
+                </li>
+            {/each}
+        </ul>
+    </div>
+
+    <div class="pros-cons">
+        <div>
+            <h4>Плюсы</h4>
+            <ul class="pros">
+                {#each result.pros as pro}<li>{pro}</li>{/each}
+            </ul>
+        </div>
+        <div>
+            <h4>Минусы</h4>
+            <ul class="cons">
+                {#each result.cons as con}<li>{con}</li>{/each}
+            </ul>
+        </div>
+    </div>
+
+    {#if result.warning}
+        <div class="warning">
+            <strong>Внимание:</strong>
+            {result.warning}
+        </div>
+    {/if}
+
+    <button onclick={reset} class="reset">Начать заново</button>
+</div>
+
+<style>
+    .result-card {
+        background: #f9f9f9;
+        padding: 1.5rem;
+        border-radius: 12px;
+    }
+    .section {
+        margin: 1rem 0;
+    }
+    .pros-cons {
+        display: grid;
+        grid-template-columns: 1fr 1fr;
+        gap: 1rem;
+    }
+    .warning {
+        background: #fff3cd;
+        padding: 1rem;
+        border-radius: 8px;
+        margin: 1rem 0;
+    }
+    .reset {
+        margin-top: 1.5rem;
+    }
+</style>

+ 41 - 0
src/lib/components/Step1_Founders.svelte

@@ -0,0 +1,41 @@
+<script>
+    let { value = $bindable(), next = $bindable() } = $props();
+</script>
+
+<div class="step">
+    <h3>1. Кто запускает бизнес?</h3>
+    <div class="options">
+        <label>
+            <input type="radio" bind:group={value} value={1} />
+            <span>1 человек</span>
+        </label>
+        <label>
+            <input type="radio" bind:group={value} value={2} />
+            <span>2–50 человек</span>
+        </label>
+        <label>
+            <input type="radio" bind:group={value} value={51} />
+            <span>Больше 50 человек</span>
+        </label>
+    </div>
+    <button onclick={next} disabled={!value}>Далее →</button>
+</div>
+
+<style>
+    .options {
+        display: flex;
+        flex-direction: column;
+        gap: 1rem;
+        margin: 1.5rem 0;
+    }
+    label {
+        display: flex;
+        align-items: center;
+        gap: 0.5rem;
+        font-size: 1.1rem;
+    }
+    button {
+        margin-top: 1rem;
+        padding: 0.75rem 1.5rem;
+    }
+</style>

+ 91 - 0
src/lib/components/Step2_Scale.svelte

@@ -0,0 +1,91 @@
+<script>
+    let {
+        value = $bindable(),
+        next = $bindable(),
+        prev = $bindable(),
+    } = $props();
+
+    const scales = [
+        {
+            id: "малый",
+            title: "Малый",
+            desc: "До 15 млн ₽/год, до 100 сотрудников",
+        },
+        { id: "средний", title: "Средний", desc: "15–800 млн ₽/год" },
+        { id: "крупный", title: "Крупный", desc: "Более 800 млн ₽/год" },
+    ];
+</script>
+
+<div class="step">
+    <h3>2. Какой масштаб планируете?</h3>
+    <p class="hint">Выберите ближайший по обороту и штату</p>
+
+    <div class="options">
+        {#each scales as scale}
+            <label class="card" class:selected={value === scale.id}>
+                <input type="radio" bind:group={value} value={scale.id} />
+                <div class="content">
+                    <strong>{scale.title}</strong>
+                    <small>{scale.desc}</small>
+                </div>
+            </label>
+        {/each}
+    </div>
+
+    <div class="actions">
+        <button onclick={prev} class="secondary">Назад</button>
+        <button onclick={next} disabled={!value}>Далее</button>
+    </div>
+</div>
+
+<style>
+    .hint {
+        color: #666;
+        font-size: 0.9rem;
+        margin-bottom: 1rem;
+    }
+    .options {
+        display: flex;
+        flex-direction: column;
+        gap: 1rem;
+        margin: 1.5rem 0;
+    }
+    .card {
+        display: flex;
+        align-items: center;
+        gap: 1rem;
+        padding: 1rem;
+        border: 2px solid #ddd;
+        border-radius: 12px;
+        cursor: pointer;
+        transition: all 0.2s;
+    }
+    .card:hover {
+        border-color: #2563eb;
+    }
+    .card.selected {
+        border-color: #2563eb;
+        background: #ebf3ff;
+    }
+    .content {
+        flex: 1;
+    }
+    small {
+        color: #555;
+        display: block;
+        margin-top: 0.25rem;
+    }
+    .actions {
+        display: flex;
+        justify-content: space-between;
+        margin-top: 2rem;
+    }
+    button {
+        padding: 0.75rem 1.5rem;
+        border-radius: 8px;
+    }
+    .secondary {
+        background: #f1f5f9;
+        color: #333;
+    }
+</style>

+ 108 - 0
src/lib/components/Step3_Activity.svelte

@@ -0,0 +1,108 @@
+<script>
+    let {
+        value = $bindable(),
+        next = $bindable(),
+        prev = $bindable(),
+    } = $props();
+
+    const activities = [
+        {
+            id: "услуги",
+            title: "Услуги",
+            examples: "Консультации, ремонт, обучение",
+        },
+        {
+            id: "торговля",
+            title: "Торговля",
+            examples: "Магазин, маркетплейс, опт",
+        },
+        {
+            id: "производство",
+            title: "Производство",
+            examples: "Еда, мебель, оборудование",
+        },
+        { id: "IT", title: "IT / Стартап", examples: "ПО, приложения, SaaS" },
+    ];
+</script>
+
+<div class="step">
+    <h3>3. Чем будете заниматься?</h3>
+    <p class="hint">Выберите основной вид деятельности</p>
+
+    <div class="grid">
+        {#each activities as act}
+            <label class="tile" class:selected={value === act.id}>
+                <input type="radio" bind:group={value} value={act.id} />
+                <div class="icon">Icon</div>
+                <strong>{act.title}</strong>
+                <small>{act.examples}</small>
+            </label>
+        {/each}
+    </div>
+
+    <div class="actions">
+        <button onclick={prev} class="secondary">Назад</button>
+        <button onclick={next} disabled={!value}>Далее</button>
+    </div>
+</div>
+
+<style>
+    .hint {
+        color: #666;
+        margin-bottom: 1rem;
+    }
+    .grid {
+        display: grid;
+        grid-template-columns: repeat(2, 1fr);
+        gap: 1rem;
+        margin: 1.5rem 0;
+    }
+    @media (max-width: 600px) {
+        .grid {
+            grid-template-columns: 1fr;
+        }
+    }
+    .tile {
+        text-align: center;
+        padding: 1.5rem 1rem;
+        border: 2px solid #e2e8f0;
+        border-radius: 16px;
+        cursor: pointer;
+        transition: all 0.2s;
+        background: white;
+    }
+    .tile:hover {
+        border-color: #2563eb;
+        transform: translateY(-2px);
+    }
+    .tile.selected {
+        border-color: #2563eb;
+        background: #ebf3ff;
+    }
+    .icon {
+        width: 40px;
+        height: 40px;
+        background: #cbd5e1;
+        border-radius: 50%;
+        margin: 0 auto 0.75rem;
+    }
+    small {
+        color: #64748b;
+        font-size: 0.85rem;
+        display: block;
+        margin-top: 0.5rem;
+    }
+    .actions {
+        display: flex;
+        justify-content: space-between;
+        margin-top: 2rem;
+    }
+    button {
+        padding: 0.75rem 1.5rem;
+        border-radius: 8px;
+    }
+    .secondary {
+        background: #f8fafc;
+        color: #334155;
+    }
+</style>

+ 107 - 0
src/lib/components/Step4_Finance.svelte

@@ -0,0 +1,107 @@
+<script>
+    let {
+        form = $bindable(),
+        next = $bindable(),
+        prev = $bindable(),
+    } = $props();
+
+    let isValid = $derived(
+        () => form.investments !== undefined && form.risk !== undefined,
+    );
+</script>
+
+<div class="step">
+    <h3>4. Финансы и ответственность</h3>
+    <p class="hint">Честно ответьте — это важно для защиты</p>
+
+    <div class="checkboxes">
+        <label class="checkbox-item">
+            <input type="checkbox" bind:checked={form.investments} />
+            <div class="label-text">
+                <strong>Нужны инвесторы или продажа доли?</strong>
+                <small>Венчур, бизнес-ангелы, партнёрство</small>
+            </div>
+        </label>
+
+        <label class="checkbox-item">
+            <input type="checkbox" bind:checked={form.risk} />
+            <div class="label-text">
+                <strong>Готов рисковать личным имуществом?</strong>
+                <small>Машина, дача, вклады — под угрозой при долгах</small>
+            </div>
+        </label>
+    </div>
+
+    {#if !form.risk}
+        <div class="warning-box">
+            <strong>Внимание:</strong> Вы выбрали "не готов" — будет рекомендована
+            форма с ограниченной ответственностью.
+        </div>
+    {/if}
+
+    <div class="actions">
+        <button onclick={prev} class="secondary">Назад</button>
+        <button onclick={next} disabled={!isValid} class="primary">
+            Получить рекомендацию
+        </button>
+    </div>
+</div>
+
+<style>
+    .hint {
+        color: #666;
+        margin-bottom: 1.5rem;
+    }
+    .checkboxes {
+        display: flex;
+        flex-direction: column;
+        gap: 1.5rem;
+        margin: 1.5rem 0;
+    }
+    .checkbox-item {
+        display: flex;
+        align-items: flex-start;
+        gap: 1rem;
+        padding: 1rem;
+        border: 1px solid #e2e8f0;
+        border-radius: 12px;
+        background: #f8fafc;
+        cursor: pointer;
+    }
+    .checkbox-item input {
+        margin-top: 0.25rem;
+    }
+    .label-text strong {
+        display: block;
+        margin-bottom: 0.25rem;
+    }
+    .label-text small {
+        color: #64748b;
+    }
+    .warning-box {
+        background: #fef3c7;
+        color: #92400e;
+        padding: 1rem;
+        border-radius: 8px;
+        margin: 1.5rem 0;
+        font-size: 0.95rem;
+    }
+    .actions {
+        display: flex;
+        justify-content: space-between;
+        margin-top: 2rem;
+    }
+    button {
+        padding: 0.75rem 1.5rem;
+        border-radius: 8px;
+    }
+    .secondary {
+        background: #e2e8f0;
+        color: #1e293b;
+    }
+    .primary {
+        background: #2563eb;
+        color: white;
+        font-weight: 600;
+    }
+</style>

+ 90 - 0
src/lib/components/Wizard.svelte

@@ -0,0 +1,90 @@
+<!-- Wizard.svelte -->
+<script>
+    import { fade, slide } from "svelte/transition";
+    import ProgressBar from "./ProgressBar.svelte";
+    import Step1_Founders from "./Step1_Founders.svelte";
+    import Step2_Scale from "./Step2_Scale.svelte";
+    import Step3_Activity from "./Step3_Activity.svelte";
+    import Step4_Finance from "./Step4_Finance.svelte";
+    import ResultCard from "./ResultCard.svelte";
+    import { findRecommendation } from "../utils/matcher";
+    import rules from "../data/rules.json";
+
+    // TODO: товарищества?, расширить линейку
+
+    let step = $state(1);
+    let form = $state({
+        founders: 1,
+        scale: "малый",
+        activity: "услуги",
+        investments: false,
+        risk: true,
+    });
+    let result = $state(null);
+    $inspect(result);
+
+    $effect(() => {
+        if (step === 5) {
+            result = findRecommendation(form, rules);
+        }
+    });
+
+    function next() {
+        if (step < 5) step++;
+    }
+
+    function prev() {
+        if (step > 1) step--;
+    }
+
+    function reset() {
+        step = 1;
+        result = null;
+        form = {
+            founders: 1,
+            scale: "малый",
+            activity: "услуги",
+            investments: false,
+            risk: true,
+        };
+    }
+
+    const steps = [
+        { num: 1, title: "Кто запускает?" },
+        { num: 2, title: "Масштаб" },
+        { num: 3, title: "Деятельность" },
+        { num: 4, title: "Финансы и риски" },
+        { num: 5, title: "Результат" },
+    ];
+</script>
+
+<div class="wizard container" transition:slide>
+    <h1 class="title">Выбор ОПФ для бизнеса</h1>
+    <ProgressBar {step} total={5} />
+
+    {#if step === 1}
+        <Step1_Founders bind:value={form.founders} {next} />
+    {:else if step === 2}
+        <Step2_Scale bind:value={form.scale} {next} {prev} />
+    {:else if step === 3}
+        <Step3_Activity bind:value={form.activity} {next} {prev} />
+    {:else if step === 4}
+        <Step4_Finance bind:form {next} {prev} />
+    {:else if step === 5 && result}
+        <ResultCard {result} {reset} />
+    {/if}
+</div>
+
+<style>
+    .wizard {
+        max-width: 600px;
+        margin: 2rem auto;
+        padding: 2rem;
+        background-color: #242627ff;
+        border-radius: 5%;
+    }
+    .title {
+        text-align: center;
+        margin-bottom: 1rem;
+    }
+</style>

+ 281 - 0
src/lib/data/rules.json

@@ -0,0 +1,281 @@
+[
+    {
+        "id": 1,
+        "conditions": {
+            "founders": 1,
+            "scale": "малый",
+            "activity": [
+                "услуги",
+                "торговля",
+                "IT"
+            ],
+            "investments": false,
+            "risk": true
+        },
+        "recommendation": "ИП",
+        "description": "Простейшая форма для одного человека. Минимум бюрократии, низкие налоги.",
+        "taxes": [
+            {
+                "name": "УСН «Доходы»",
+                "rate": "6%",
+                "note": "Можно вычесть страховые взносы (до 100%)"
+            },
+            {
+                "name": "Патент",
+                "rate": "Фикс. стоимость",
+                "note": "Только для 63 видов деятельности"
+            },
+            {
+                "name": "НПД (самозанятый)",
+                "rate": "4–6%",
+                "note": "До 2.4 млн ₽/год, без взносов"
+            }
+        ],
+        "pros": [
+            "Регистрация за 3 дня",
+            "Нет уставного капитала",
+            "Минимум отчётности",
+            "Можно применять УСН, патент, НПД"
+        ],
+        "cons": [
+            "Полная ответственность (всё имущество, кроме жилья)",
+            "Нельзя привлечь инвесторов",
+            "Нельзя продать бизнес"
+        ],
+        "warning": "При долгах — рискуете квартирой, машиной, дачей. Жильё защищено только если оно единственное."
+    },
+    {
+        "id": 2,
+        "conditions": {
+            "founders": 1,
+            "scale": "малый",
+            "activity": [
+                "услуги",
+                "торговля",
+                "IT"
+            ],
+            "investments": false,
+            "risk": false
+        },
+        "recommendation": "ООО",
+        "description": "Для одного учредителя, но с защитой имущества. Подходит, если боитесь рисков.",
+        "taxes": [
+            {
+                "name": "УСН «Доходы»",
+                "rate": "6%",
+                "note": "Взносы за сотрудников"
+            },
+            {
+                "name": "УСН «Доходы − Расходы»",
+                "rate": "15%",
+                "note": "Выгодно при высоких расходах"
+            }
+        ],
+        "pros": [
+            "Ограниченная ответственность (только в пределах уставного капитала)",
+            "Можно продать долю",
+            "Гибкость в управлении"
+        ],
+        "cons": [
+            "Уставной капитал ≥ 10 000 ₽",
+            "Больше отчётности (ежеквартально)",
+            "Регистрация 5–7 дней"
+        ],
+        "warning": "Если вы — единственный учредитель и директор, суд может «проколоть корпоративную завесу» при умышленном банкротстве."
+    },
+    {
+        "id": 3,
+        "conditions": {
+            "founders": {
+                "min": 2,
+                "max": 50
+            },
+            "investments": false,
+            "scale": [
+                "малый",
+                "средний"
+            ]
+        },
+        "recommendation": "ООО",
+        "description": "Классика для партнёрства. Чёткое распределение долей, защита имущества.",
+        "taxes": [
+            {
+                "name": "УСН «Доходы»",
+                "rate": "6%",
+                "note": "Простой учёт"
+            },
+            {
+                "name": "УСН «Доходы − Расходы»",
+                "rate": "15%",
+                "note": "При расходах >60%"
+            }
+        ],
+        "pros": [
+            "Ограниченная ответственность",
+            "Можно продать/передать долю",
+            "Гибкое распределение прибыли"
+        ],
+        "cons": [
+            "Нужен устав, договор",
+            "Сложнее выход учредителя",
+            "Налоги на дивиденды (13%)"
+        ],
+        "warning": "Конфликты между учредителями — главная причина закрытия ООО."
+    },
+    {
+        "id": 4,
+        "conditions": {
+            "investments": true
+        },
+        "recommendation": "ООО",
+        "description": "Лучший выбор для привлечения инвестиций. Можно продать долю, выдать опционы.",
+        "taxes": [
+            {
+                "name": "УСН",
+                "rate": "6% или 15%",
+                "note": "До 219.2 млн ₽/год"
+            },
+            {
+                "name": "ОСНО",
+                "rate": "20% + НДС 20%",
+                "note": "Если инвесторы требуют НДС"
+            }
+        ],
+        "pros": [
+            "Легко привлечь инвесторов",
+            "Можно выдать опционы сотрудникам",
+            "Защита имущества"
+        ],
+        "cons": [
+            "Сложнее структура",
+            "Налоги на дивиденды",
+            "Больше контроля со стороны инвесторов"
+        ],
+        "warning": "Инвесторы потребуют долю и место в совете директоров."
+    },
+    {
+        "id": 5,
+        "conditions": {
+            "founders": {
+                "min": 51
+            },
+            "scale": "крупный",
+            "activity": [
+                "производство",
+                "торговля"
+            ]
+        },
+        "recommendation": "АО (НАО или ПАО)",
+        "description": "Для крупного бизнеса с акциями. Можно выйти на биржу.",
+        "taxes": [
+            {
+                "name": "ОСНО",
+                "rate": "20% + НДС 20%",
+                "note": "Обязательно"
+            }
+        ],
+        "pros": [
+            "Привлечение через акции",
+            "Выход на IPO",
+            "Ограниченная ответственность"
+        ],
+        "cons": [
+            "Уставной капитал ≥ 100 000 ₽ (НАО) / 1 000 000 ₽ (ПАО)",
+            "Сложная отчётность",
+            "Регулирование ЦБ РФ"
+        ],
+        "warning": "ПАО — публичная отчётность, дорого и сложно."
+    },
+    {
+        "id": 6,
+        "conditions": {
+            "activity": "IT",
+            "investments": true,
+            "scale": [
+                "малый",
+                "средний"
+            ]
+        },
+        "recommendation": "ООО + опционы",
+        "description": "Стандарт для IT-стартапов. Можно мотивировать команду опционами.",
+        "taxes": [
+            {
+                "name": "УСН",
+                "rate": "6% или 15%",
+                "note": "Льготы для IT (страховые взносы 7.6%)"
+            }
+        ],
+        "pros": [
+            "Льготы для аккредитованных IT-компаний",
+            "Опционы для сотрудников",
+            "Инвесторы любят ООО"
+        ],
+        "cons": [
+            "Нужно получить аккредитацию Минцифры",
+            "Дивиденды облагаются 13%"
+        ],
+        "warning": "Без аккредитации — нет льгот."
+    },
+    {
+        "id": 7,
+        "conditions": {
+            "activity": "производство",
+            "scale": "средний"
+        },
+        "recommendation": "ООО",
+        "description": "Для среднего производства с оборудованием и кредитами.",
+        "taxes": [
+            {
+                "name": "УСН «Доходы − Расходы»",
+                "rate": "15%",
+                "note": "Выгодно при высоких затратах"
+            },
+            {
+                "name": "ОСНО",
+                "rate": "20% + НДС",
+                "note": "Если работаете с крупными заказчиками"
+            }
+        ],
+        "pros": [
+            "Можно взять кредит под оборудование",
+            "Ограниченная ответственность",
+            "Зачёт НДС"
+        ],
+        "cons": [
+            "Сложный учёт",
+            "Больше проверок"
+        ],
+        "warning": "ОСНО — если клиенты требуют НДС."
+    },
+    {
+        "id": 8,
+        "conditions": {
+            "founders": 1,
+            "scale": "малый",
+            "activity": "услуги",
+            "investments": false,
+            "risk": true,
+            "income_limit": true
+        },
+        "recommendation": "Самозанятый (НПД)",
+        "description": "Самая простая форма. Без отчётов, без взносов.",
+        "taxes": [
+            {
+                "name": "НПД",
+                "rate": "4% (физлица) / 6% (юрлица)",
+                "note": "До 2.4 млн ₽/год"
+            }
+        ],
+        "pros": [
+            "Налоги через приложение",
+            "Нет страховых взносов",
+            "Можно совмещать с работой"
+        ],
+        "cons": [
+            "До 2.4 млн ₽/год",
+            "Нельзя нанимать сотрудников",
+            "Нет вычета НДС"
+        ],
+        "warning": "При превышении лимита — автоматом ИП или закрытие."
+    }
+]

+ 34 - 0
src/lib/utils/matcher.js

@@ -0,0 +1,34 @@
+export function findRecommendation(input, rules) {
+    return rules.find(rule => matches(rule.conditions, input));
+}
+
+function matches(conditions, input) {
+    return (
+        checkNumber(conditions.founders, input.founders) &&
+        checkArrayOrString(conditions.scale, input.scale) &&
+        checkArrayOrString(conditions.activity, input.activity) &&
+        checkBool(conditions.investments, input.investments) &&
+        checkBool(conditions.risk, input.risk)
+    );
+}
+
+function checkNumber(cond, val) {
+    if (cond === undefined) return true;
+    if (typeof cond === 'object') {
+        const min = cond.min || 0;
+        const max = cond.max || Infinity;
+        return val >= min && val <= max;
+    }
+    return cond === val;
+}
+
+function checkArrayOrString(cond, val) {
+    if (!cond) return true;
+    if (Array.isArray(cond)) return cond.includes(val);
+    return cond === val;
+}
+
+function checkBool(cond, val) {
+    if (cond === undefined || cond === null) return true;
+    return cond === val;
+}