init 5S article and refacto pull system
This commit is contained in:
61
src/modules/pull-system/FlowArticle.vue
Normal file
61
src/modules/pull-system/FlowArticle.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import SeparatorIcon from '@/icons/SeparatorIcon.vue'
|
||||
import FlowConclusion from '@/modules/pull-system/article/FlowConclusion.vue'
|
||||
import FlowHypothesis from '@/modules/pull-system/article/FlowHypothesis.vue'
|
||||
import FlowIntro from '@/modules/pull-system/article/FlowIntro.vue'
|
||||
import FlowMultipleSimulation from '@/modules/pull-system/article/FlowMultipleSimulation.vue'
|
||||
import FlowSetup from '@/modules/pull-system/article/FlowSetup.vue'
|
||||
import FlowSingleSimulation from '@/modules/pull-system/article/FlowSingleSimulation.vue'
|
||||
import FeatureSteps from '@/modules/pull-system/feature/FeatureSteps.vue'
|
||||
import FlowDashboard from '@/modules/pull-system/feature/FlowDashboard.vue'
|
||||
import SimulationControls from '@/modules/pull-system/simulation/SimulationControls.vue'
|
||||
import SimulationDashboard from '@/modules/pull-system/simulation/SimulationDashboard.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flow-article">
|
||||
<h1>Pull system</h1>
|
||||
<h2>Ekiden (駅伝): long-distance running relay race</h2>
|
||||
<FlowIntro class="text" />
|
||||
<SeparatorIcon />
|
||||
<FlowHypothesis class="text" />
|
||||
<SeparatorIcon />
|
||||
<FlowSetup class="text" />
|
||||
<FlowDashboard class="above" />
|
||||
<FeatureSteps alias="playground" />
|
||||
<SimulationControls type="single" class="above" />
|
||||
<SimulationDashboard />
|
||||
<FlowSingleSimulation class="text" />
|
||||
<SimulationControls type="multiple" class="above" />
|
||||
<SimulationDashboard />
|
||||
<FlowMultipleSimulation class="text" />
|
||||
<SeparatorIcon />
|
||||
<FlowConclusion class="text" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.flow-article {
|
||||
color: black;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 1080px;
|
||||
margin: auto;
|
||||
|
||||
.text {
|
||||
font-size: 16pt;
|
||||
max-width: 650px;
|
||||
margin: auto;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.above {
|
||||
z-index: 1;
|
||||
padding-top: 1rem;
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
24
src/modules/pull-system/article/FlowConclusion.vue
Normal file
24
src/modules/pull-system/article/FlowConclusion.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="flow-conclusion">
|
||||
<p>
|
||||
When money and pressure are in the game, fear, uncertainty, and doubt
|
||||
spread out rapidly. So we rush, as fast as we can, and when a team has
|
||||
nothing to do, it becomes a disaster: money are being burned.
|
||||
</p>
|
||||
<p>
|
||||
Teams will overproduce. They will do every pieces they can as they are
|
||||
autonomous: product team will pitch their features, designers will design
|
||||
every pages, developers will rush to code forgetting why the customer
|
||||
needs this precise feature. They will then struggle to get these pieces to
|
||||
the very end when they will transfer them to the others teams. So the fear
|
||||
of having nothing to give to the next team pushes us to produce just in
|
||||
case. Bugs lead to more bugs that lead to more bugs. Multitasking will
|
||||
drastically drop the productivity.
|
||||
</p>
|
||||
<p>
|
||||
But the best way to succeed is to ask: "Is this what you need? How can I
|
||||
help you?". Just like in Ekiden, handovers are where the real struggles
|
||||
happen but we're on the same race, together.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
32
src/modules/pull-system/article/FlowHypothesis.vue
Normal file
32
src/modules/pull-system/article/FlowHypothesis.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div class="flow-hypothesis">
|
||||
<p>Here our hypothesis:</p>
|
||||
<ol>
|
||||
<li>
|
||||
it takes the same amount of time for each team to complete a task
|
||||
<span class="meaning">same task time</span>
|
||||
</li>
|
||||
<li>teams have no other external dependencies</li>
|
||||
<li>
|
||||
teams know exactly what they need to produce their part, they will tag
|
||||
any defects they found when verifying the feature is good.
|
||||
</li>
|
||||
<li>
|
||||
0 defect policy: the team where the defect appears must rework the
|
||||
feature.
|
||||
</li>
|
||||
<li>release team never fails</li>
|
||||
<li>there is no limit on how many defects a feature can have.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
ol {
|
||||
li {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
13
src/modules/pull-system/article/FlowIntro.vue
Normal file
13
src/modules/pull-system/article/FlowIntro.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div class="flow-intro">
|
||||
<h2>What's the fastest way to deliver an app?</h2>
|
||||
<p>
|
||||
How good are we to succeed a project? Delivering the features at the good
|
||||
quality and at the right time. We're pretty bad at it and there is a lot
|
||||
of misconceptions on how to manage people to do what need to be done. I
|
||||
wanted to create a simulation to plan ahead and see what are the
|
||||
consequences of different strategy patterns and which is the most
|
||||
effective.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
24
src/modules/pull-system/article/FlowMultipleSimulation.vue
Normal file
24
src/modules/pull-system/article/FlowMultipleSimulation.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div class="flow-conclusion">
|
||||
<p>
|
||||
Teams tend to underestimate how long a project will be. And how hard it
|
||||
will be to work with others.
|
||||
</p>
|
||||
<p>
|
||||
If we're not in a good pace, we just have to try harder. Only once. "Just
|
||||
in time" becomes "Just this time" many times. So teams overproduce.
|
||||
Creating stock and latent defects the teams need to rework. The worse the
|
||||
project do, the more silot we become and we tend to argue with a "I've
|
||||
done my job, if the project fails it's not my fault.". The fact is that
|
||||
it's nobody's fault, it's a system.
|
||||
</p>
|
||||
<p>
|
||||
Short term objectives become the norm. It is never the right time to do
|
||||
problem solving. To think on how we work.
|
||||
</p>
|
||||
<p>
|
||||
Pull system must be a constrain not a choice with the idea to build
|
||||
around.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
106
src/modules/pull-system/article/FlowSetup.vue
Normal file
106
src/modules/pull-system/article/FlowSetup.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
import ProblemSolvingIcon from '@/icons/ProblemSolvingIcon.vue'
|
||||
import PullSystemIcon from '@/icons/PullSystemIcon.vue'
|
||||
import PushSystemIcon from '@/icons/PushSystemIcon.vue'
|
||||
import FeatureItem from '@/modules/pull-system/feature/FeatureItem.vue'
|
||||
import FlowControls from '@/modules/pull-system/feature/FlowControls.vue'
|
||||
import QualityIssue from '@/modules/pull-system/feature/QualityIssue.vue'
|
||||
import { Feature } from '@/modules/pull-system/feature/feature'
|
||||
|
||||
const feature: Feature = {
|
||||
name: 'As a user, in the homepage, I can login',
|
||||
complexity: 3,
|
||||
leadTime: 2,
|
||||
qualityIssue: 4,
|
||||
status: 'doing',
|
||||
step: 2
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p>
|
||||
You're a Product Manager in a project who has just started, your goal is
|
||||
to make a product as fast as you can. With you, you'll have the product
|
||||
team, designers, developers and a release team.
|
||||
</p>
|
||||
<p>This is a feature:</p>
|
||||
<FeatureItem :feature="feature" />
|
||||
<p>
|
||||
<span class="numeric">({{ feature.complexity }})</span> is the complexity
|
||||
of the feature. The more complex a feature is, the more chance to have
|
||||
defect we have.
|
||||
</p>
|
||||
<p>
|
||||
<span class="numeric">{{ feature.leadTime }}d</span> is the number of days
|
||||
the feature started its journey. The ultimate goal is to reduce this
|
||||
number and deliver as fast as possible.
|
||||
</p>
|
||||
<p>
|
||||
<QualityIssue class="inline" :quality-issue="feature.qualityIssue" />
|
||||
are the number of defects the feature had during the flow.
|
||||
</p>
|
||||
<p>
|
||||
You have 20 features to deliver, and each day you can choose between 3
|
||||
strategies:
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
Push system
|
||||
<PushSystemIcon />
|
||||
</li>
|
||||
<li>
|
||||
Pull system
|
||||
<PullSystemIcon />
|
||||
</li>
|
||||
<li>
|
||||
Problem solving
|
||||
<ProblemSolvingIcon />
|
||||
</li>
|
||||
</ol>
|
||||
<FlowControls :with-eraser="false" />
|
||||
<p>
|
||||
In this article we'll focus on how these strategies are efficient and what
|
||||
are the impact on the quality the teams produce.
|
||||
</p>
|
||||
<h3>The push system: start features as many as possible</h3>
|
||||
<h4>Seeking for the most of each team</h4>
|
||||
<p>
|
||||
Pushing all the feature as fast as possible, this is the only strategy
|
||||
that allows us to target the best possible result: `# step × # status × #
|
||||
features`. We may overburden teams in the process though. But as we
|
||||
already invest money everybody to work so if there is someone who has
|
||||
nothing to do, this is just money down the drain.
|
||||
</p>
|
||||
<h3>The pull system: produce features only when the next team needs it</h3>
|
||||
<p>
|
||||
It comes from the assumptions that we will never reach the best score ever
|
||||
aka "the push system where everything goes right". We know we prefer
|
||||
waiting for next team to be ready before doing some extra work than having
|
||||
stock of feature pre-baked.
|
||||
</p>
|
||||
<h3>Problem solving: no production, just reflection</h3>
|
||||
<p>
|
||||
We invest days where we are not productive at all to investigate and
|
||||
learning from our mistake. We know that we will never reach the best score
|
||||
ever and we know that mistakes will reappear. So we take more time to
|
||||
understand and limit rework. The more the team investigate, the more the
|
||||
team learn and start to be extremely good at problem solving.
|
||||
</p>
|
||||
<h3>Blue bin: the security stock</h3>
|
||||
<p>
|
||||
Blue bins are your security stock, they make sure teams can work without
|
||||
any blockers. The next team will always have material to transform. But it
|
||||
comes with a cost: too many blue bins add overburden, stagnation
|
||||
(increasing lead time) and duplicated mistakes. The less you have, the
|
||||
more the team can focus. But the more you have, the more secure you are to
|
||||
make teams work, we have to find the good balance.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.inline {
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
||||
9
src/modules/pull-system/article/FlowSingleSimulation.vue
Normal file
9
src/modules/pull-system/article/FlowSingleSimulation.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div class="flow-conclusion">
|
||||
<p>Pull system wins!</p>
|
||||
<p>
|
||||
Let's make sure it wasn't luck and generate 200 projects delivering 200
|
||||
features each! And see what happens.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
45
src/modules/pull-system/feature/FeatureItem.vue
Normal file
45
src/modules/pull-system/feature/FeatureItem.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import QualityIssue from '@/modules/pull-system/feature/QualityIssue.vue'
|
||||
import { Feature } from '@/modules/pull-system/feature/feature'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{ feature: Feature; isLive?: boolean }>(),
|
||||
{ isLive: false }
|
||||
)
|
||||
const hasQualityIssues = computed(() => props.feature.qualityIssue > 0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="feature-item bin" :class="{ live: isLive }">
|
||||
<div>
|
||||
<span class="numeric">({{ feature.complexity }})</span> {{ feature.name }}
|
||||
</div>
|
||||
<div class="numeric">
|
||||
{{ feature.leadTime }}d
|
||||
<QualityIssue
|
||||
v-if="hasQualityIssues"
|
||||
:quality-issue="feature.qualityIssue"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.feature-item {
|
||||
transition-property: opacity, visibility;
|
||||
transition-duration: 0.9s;
|
||||
transition-timing-function: ease-out;
|
||||
transition-delay: 1s 2s;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
justify-content: space-around;
|
||||
|
||||
&.live {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
175
src/modules/pull-system/feature/FeatureStep.vue
Normal file
175
src/modules/pull-system/feature/FeatureStep.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<script setup lang="ts">
|
||||
import FeatureItem from '@/modules/pull-system/feature/FeatureItem.vue'
|
||||
import { Feature } from '@/modules/pull-system/feature/feature'
|
||||
import { FeatureStep } from '@/modules/pull-system/feature/feature-steps'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import { computed, ref } from 'vue'
|
||||
import { Starport } from 'vue-starport'
|
||||
|
||||
const props = defineProps<{
|
||||
prefix: string
|
||||
step: FeatureStep
|
||||
features: Feature[]
|
||||
}>()
|
||||
const featuresInProgress = computed(() =>
|
||||
props.features.filter((feature) => feature.status === 'doing')
|
||||
)
|
||||
const featuresDone = computed(() =>
|
||||
props.features
|
||||
.filter((feature) => feature.status === 'done')
|
||||
.sort((a, b) => (a.leadTime > b.leadTime ? -1 : 1))
|
||||
)
|
||||
|
||||
const hasFeaturesInProgress = computed(
|
||||
() => featuresInProgress.value.length > 0
|
||||
)
|
||||
const isLive = computed(
|
||||
() => props.step.title.toLocaleLowerCase() === 'release'
|
||||
)
|
||||
|
||||
const binContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
const { width } = useElementSize(binContainer)
|
||||
|
||||
const binContainerWidth = computed(() => {
|
||||
if (!width) {
|
||||
return ''
|
||||
}
|
||||
return `width: ${width.value}px`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li class="feature-step">
|
||||
<header>{{ step.title }}</header>
|
||||
<section class="doing">
|
||||
<h5>⚒️ [{{ featuresInProgress.length }}]</h5>
|
||||
<ul v-if="hasFeaturesInProgress">
|
||||
<li v-for="feature in featuresInProgress" :key="feature.name">
|
||||
<Starport
|
||||
:port="`${props.prefix}-${feature.name}`"
|
||||
style="height: var(--feature-item-height)"
|
||||
>
|
||||
<FeatureItem :feature="feature" />
|
||||
</Starport>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section class="done">
|
||||
<h5>
|
||||
✅ [{{ featuresDone.length
|
||||
}}<template v-if="!isLive">/{{ step.blueBins }}</template
|
||||
>]
|
||||
</h5>
|
||||
<div ref="binContainer">
|
||||
<div
|
||||
v-if="!isLive"
|
||||
class="blue-bin-container"
|
||||
:style="binContainerWidth"
|
||||
>
|
||||
<div
|
||||
v-for="blueBin in step.blueBins"
|
||||
:key="blueBin"
|
||||
class="bin blue-bin"
|
||||
>
|
||||
blue bin
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isLive" class="live">
|
||||
<span v-if="featuresDone.length === 0">No features live yet</span>
|
||||
<span v-else>
|
||||
{{ featuresDone.length }} feature<template
|
||||
v-if="featuresDone.length > 1"
|
||||
>s</template
|
||||
>
|
||||
live!
|
||||
</span>
|
||||
</div>
|
||||
<ul class="done-list">
|
||||
<li v-for="feature in featuresDone" :key="feature.name">
|
||||
<Starport
|
||||
:port="`${props.prefix}-${feature.name}`"
|
||||
style="height: var(--feature-item-height)"
|
||||
>
|
||||
<FeatureItem :feature="feature" :is-live="isLive" />
|
||||
</Starport>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@mixin hideScrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 0 !important;
|
||||
}
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.feature-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
header {
|
||||
padding: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
section {
|
||||
@include hideScrollbar();
|
||||
margin: 1rem 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: hidden;
|
||||
max-height: 40vh;
|
||||
}
|
||||
|
||||
h5 {
|
||||
margin: 0;
|
||||
background-color: var(--primary-color);
|
||||
padding: 0.35rem;
|
||||
text-align: center;
|
||||
color: var(--color);
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
li {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.bin {
|
||||
height: var(--feature-item-height);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.blue-bin-container {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* mobile screen */
|
||||
@media only screen and (max-width: 750px) {
|
||||
.blue-bin-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.live {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
62
src/modules/pull-system/feature/FeatureSteps.vue
Normal file
62
src/modules/pull-system/feature/FeatureSteps.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import FeatureStep from '@/modules/pull-system/feature/FeatureStep.vue'
|
||||
import { featureSteps } from '@/modules/pull-system/feature/feature-steps'
|
||||
import { useFeatureStore } from '@/modules/pull-system/feature/feature-store'
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
const NUMBER_OF_FEATURES = 20
|
||||
|
||||
defineProps<{ alias: string }>()
|
||||
|
||||
const featureStore = useFeatureStore()
|
||||
|
||||
onMounted(() => featureStore.initBoard(NUMBER_OF_FEATURES))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ul class="features-steps">
|
||||
<FeatureStep
|
||||
v-for="step in featureSteps"
|
||||
:prefix="alias"
|
||||
:key="step.title"
|
||||
:step="step"
|
||||
:features="featureStore.featuresGroupedByStep[step.stepIndex] ?? []"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.features-steps {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: stretch;
|
||||
margin: 0 1rem;
|
||||
border: 3px solid var(--primary-color);
|
||||
height: 80vh;
|
||||
width: 100%;
|
||||
overflow-y: hidden;
|
||||
|
||||
li:first-child {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
li:last-child {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
li:not(:last-child) {
|
||||
padding-right: 1rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
flex: 1;
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
color: var(--primary-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
53
src/modules/pull-system/feature/FlowControls.vue
Normal file
53
src/modules/pull-system/feature/FlowControls.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import EraserIcon from '@/icons/EraserIcon.vue'
|
||||
import ProblemSolvingIcon from '@/icons/ProblemSolvingIcon.vue'
|
||||
import PullSystemIcon from '@/icons/PullSystemIcon.vue'
|
||||
import PushSystemIcon from '@/icons/PushSystemIcon.vue'
|
||||
import { useFeatureStore } from '@/modules/pull-system/feature/feature-store'
|
||||
|
||||
const NUMBER_OF_FEATURES = 20
|
||||
|
||||
withDefaults(defineProps<{ withEraser?: boolean }>(), { withEraser: true })
|
||||
|
||||
const featureStore = useFeatureStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flow-controls">
|
||||
<div class="row">
|
||||
<button
|
||||
@click="featureStore.nextDay('push')"
|
||||
:disabled="featureStore.isProjectFinished"
|
||||
>
|
||||
<PushSystemIcon color="white" />
|
||||
</button>
|
||||
<button
|
||||
@click="featureStore.nextDay('pull')"
|
||||
:disabled="featureStore.isProjectFinished"
|
||||
>
|
||||
<PullSystemIcon color="white" />
|
||||
</button>
|
||||
<button
|
||||
@click="featureStore.nextDay('problem-solving')"
|
||||
:disabled="featureStore.isProjectFinished"
|
||||
>
|
||||
<ProblemSolvingIcon color="white" />
|
||||
</button>
|
||||
<button
|
||||
v-if="withEraser"
|
||||
@click="featureStore.initBoard(NUMBER_OF_FEATURES)"
|
||||
>
|
||||
<EraserIcon color="white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.flow-controls {
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
106
src/modules/pull-system/feature/FlowDashboard.vue
Normal file
106
src/modules/pull-system/feature/FlowDashboard.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
import SeparatorIcon from '@/icons/SeparatorIcon.vue'
|
||||
import FlowControls from '@/modules/pull-system/feature/FlowControls.vue'
|
||||
import { useFeatureStore } from '@/modules/pull-system/feature/feature-store'
|
||||
|
||||
const featureStore = useFeatureStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flow-dashboard">
|
||||
<SeparatorIcon />
|
||||
<div class="row cards">
|
||||
<div class="card">
|
||||
Features
|
||||
<span class="numeric">
|
||||
{{ featureStore.totalFeaturesDone
|
||||
}}<span class="sub">/{{ featureStore.totalFeaturesCount }} </span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="card">
|
||||
Team work exp.
|
||||
<span class="numeric">
|
||||
{{ featureStore.meta.teamWorkExperience.toFixed(2) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card">
|
||||
Mean complexity
|
||||
<div class="numeric">{{ featureStore.meanComplexity }}</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
Mean lead time
|
||||
<div class="data">
|
||||
<span class="numeric">{{ featureStore.meanLeadTime }}</span>
|
||||
days
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row cards">
|
||||
<div class="card">
|
||||
Takt time
|
||||
<div class="data">
|
||||
<span class="numeric">{{ featureStore.taktTime }}</span>
|
||||
days
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
Days
|
||||
<div class="data">
|
||||
<span class="numeric">{{ featureStore.meta.totalDays }}</span
|
||||
>d
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
ETA
|
||||
<div class="data">
|
||||
<span class="numeric">{{ featureStore.eat }}</span>
|
||||
days
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FlowControls />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.flow-dashboard {
|
||||
font-size: 16pt;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
.cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-image: linear-gradient(
|
||||
335deg,
|
||||
hsl(223deg 58% 88%) 0%,
|
||||
hsl(224deg 57% 88%) 21%,
|
||||
hsl(224deg 58% 89%) 30%,
|
||||
hsl(223deg 57% 89%) 39%,
|
||||
hsl(222deg 56% 89%) 46%,
|
||||
hsl(223deg 57% 90%) 54%,
|
||||
hsl(222deg 55% 90%) 61%,
|
||||
hsl(223deg 53% 91%) 69%,
|
||||
hsl(221deg 56% 91%) 79%,
|
||||
hsl(222deg 53% 92%) 100%
|
||||
);
|
||||
padding: 0 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.numeric {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
20
src/modules/pull-system/feature/QualityIssue.vue
Normal file
20
src/modules/pull-system/feature/QualityIssue.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
qualityIssue: number
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="quality-issue red-bin numeric">{{ qualityIssue }}</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.quality-issue {
|
||||
&.red-bin {
|
||||
--warning-color: #ca0e0e;
|
||||
border: 2px solid var(--warning-color);
|
||||
padding: 0 0.5rem 0.1rem;
|
||||
color: var(--warning-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
308
src/modules/pull-system/feature/feature-board.ts
Normal file
308
src/modules/pull-system/feature/feature-board.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
import { Feature, FeatureStatus } from '@/modules/pull-system/feature/feature'
|
||||
import { FeatureStep } from '@/modules/pull-system/feature/feature-steps'
|
||||
import { features as initialFeatures } from '@/modules/pull-system/feature/feature.fixture'
|
||||
import { Strategy } from '@/modules/lean/strategy'
|
||||
import { FeatureState } from '@/store-type'
|
||||
import {
|
||||
getMean,
|
||||
pickRandomElement,
|
||||
popNElement,
|
||||
randomFloat,
|
||||
shuffleArray,
|
||||
sumElements
|
||||
} from '@/utils'
|
||||
|
||||
const HARD_STOP = 5000
|
||||
|
||||
const getQualityIssue = ({
|
||||
complexity,
|
||||
tasksInParallel,
|
||||
teamWorkExperience
|
||||
}: {
|
||||
complexity: number
|
||||
tasksInParallel: number
|
||||
teamWorkExperience: number
|
||||
}): boolean => {
|
||||
const qualityProbability = getQualityProbability(
|
||||
complexity,
|
||||
teamWorkExperience
|
||||
)
|
||||
|
||||
const multiplicator = getOverburdenMultiplicator(tasksInParallel)
|
||||
const quality = randomFloat(0, 1)
|
||||
|
||||
return quality > qualityProbability / multiplicator
|
||||
}
|
||||
|
||||
const mayBeInProgress = ({
|
||||
features,
|
||||
feature,
|
||||
strategy,
|
||||
steps
|
||||
}: {
|
||||
features: Feature[]
|
||||
feature: Feature
|
||||
steps: FeatureStep[]
|
||||
strategy: Strategy
|
||||
}): FeatureStatus => {
|
||||
if (strategy !== 'pull') {
|
||||
return 'doing'
|
||||
}
|
||||
|
||||
const nextStep = steps.find((step) => step.stepIndex === feature.step - 1)
|
||||
|
||||
if (!nextStep) {
|
||||
return feature.status
|
||||
}
|
||||
|
||||
const hasBlueBinAvailableNextStep =
|
||||
nextStep.blueBins -
|
||||
features.filter((f) => f.step === feature.step - 1).length >
|
||||
0
|
||||
|
||||
if (hasBlueBinAvailableNextStep) {
|
||||
return 'doing'
|
||||
}
|
||||
|
||||
return feature.status
|
||||
}
|
||||
|
||||
export const newBacklog = (limit?: number) =>
|
||||
limit !== undefined
|
||||
? popNElement(shuffleArray(initialFeatures), limit)
|
||||
: shuffleArray(initialFeatures)
|
||||
|
||||
export const initBoard = (
|
||||
steps: FeatureStep[],
|
||||
features: Feature[]
|
||||
): Feature[] => {
|
||||
const initialFeatures = popNElement(features, 10)
|
||||
|
||||
initialFeatures.forEach((feature) => {
|
||||
const step = pickRandomElement(steps)
|
||||
feature.status = pickRandomElement(['doing', 'done'])
|
||||
feature.step = Math.max(step.stepIndex, 1)
|
||||
})
|
||||
|
||||
return initialFeatures
|
||||
}
|
||||
|
||||
export const getFeaturesForNextDay = ({
|
||||
backlog,
|
||||
features,
|
||||
initialStep,
|
||||
steps,
|
||||
strategy,
|
||||
teamWorkExperience
|
||||
}: {
|
||||
backlog: Feature[]
|
||||
features: Feature[]
|
||||
steps: FeatureStep[]
|
||||
initialStep: number
|
||||
strategy: Strategy | 'problem-solving'
|
||||
teamWorkExperience: number
|
||||
}): [Feature[], Feature[]] => {
|
||||
features
|
||||
.filter((feature) => feature.step > 0 || feature.status === 'doing')
|
||||
.sort((a, b) => (a.step < b.step ? -1 : 1))
|
||||
.forEach((feature) => {
|
||||
feature.leadTime++
|
||||
|
||||
if (strategy === 'problem-solving') {
|
||||
return
|
||||
}
|
||||
|
||||
switch (feature.status) {
|
||||
case 'doing':
|
||||
feature.status = 'done'
|
||||
break
|
||||
case 'done':
|
||||
feature.status = mayBeInProgress({
|
||||
features,
|
||||
feature,
|
||||
steps,
|
||||
strategy
|
||||
})
|
||||
|
||||
if (feature.status === 'done') {
|
||||
break
|
||||
}
|
||||
|
||||
const hasQualityIssue = getQualityIssue({
|
||||
complexity: feature.complexity,
|
||||
tasksInParallel: features.filter(
|
||||
(f) => f.status === 'doing' && f.step === feature.step
|
||||
).length,
|
||||
teamWorkExperience
|
||||
})
|
||||
|
||||
if (hasQualityIssue) {
|
||||
feature.qualityIssue++
|
||||
} else {
|
||||
// moving to the next team
|
||||
feature.step--
|
||||
}
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
if (backlog.length > 0) {
|
||||
switch (strategy) {
|
||||
case 'push': {
|
||||
const [nextFeature] = popNElement(backlog, 1)
|
||||
|
||||
if (nextFeature) {
|
||||
features.push({ ...nextFeature, step: initialStep })
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case 'pull': {
|
||||
const firstStep = steps.find((step) => step.stepIndex === initialStep)
|
||||
if (!firstStep) {
|
||||
break
|
||||
}
|
||||
|
||||
const hasBlueBinAvailableOnFirstStep =
|
||||
firstStep.blueBins -
|
||||
features.filter(
|
||||
(f) => f.step === initialStep && f.status === 'done'
|
||||
).length >
|
||||
0
|
||||
|
||||
if (hasBlueBinAvailableOnFirstStep) {
|
||||
const [newFeature] = popNElement(backlog, 1)
|
||||
|
||||
if (newFeature) {
|
||||
features.push({ ...newFeature, step: initialStep })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [backlog, features]
|
||||
}
|
||||
|
||||
const getOverburdenMultiplicator = (tasksInParallel: number) => {
|
||||
switch (tasksInParallel) {
|
||||
case 1:
|
||||
return 1
|
||||
case 2:
|
||||
return 1.5
|
||||
case 3:
|
||||
return 5
|
||||
case 4:
|
||||
return 8
|
||||
case 5:
|
||||
return 13
|
||||
default:
|
||||
return 25
|
||||
}
|
||||
}
|
||||
|
||||
const getQualityProbability = (
|
||||
complexity: number,
|
||||
teamWorkExperience: number
|
||||
) => {
|
||||
let probabilityOfGoodQuality = 1
|
||||
|
||||
switch (complexity) {
|
||||
case 1:
|
||||
probabilityOfGoodQuality = 0.95
|
||||
|
||||
case 2:
|
||||
probabilityOfGoodQuality = 0.88
|
||||
break
|
||||
case 3:
|
||||
probabilityOfGoodQuality = 0.72
|
||||
break
|
||||
case 4:
|
||||
probabilityOfGoodQuality = 0.65
|
||||
break
|
||||
case 5:
|
||||
probabilityOfGoodQuality = 0.5
|
||||
break
|
||||
}
|
||||
|
||||
// team learning
|
||||
probabilityOfGoodQuality =
|
||||
probabilityOfGoodQuality + (1.2 * teamWorkExperience) / 100
|
||||
|
||||
return probabilityOfGoodQuality
|
||||
}
|
||||
|
||||
export const isFeatureDone = (feature: Feature) =>
|
||||
feature.step === 0 && feature.status === 'done'
|
||||
|
||||
export const nextDay = (
|
||||
state: FeatureState,
|
||||
strategy: Strategy | 'problem-solving'
|
||||
): FeatureState => {
|
||||
state.meta.totalDays++
|
||||
// each day, the teams know how to better work together
|
||||
state.meta.teamWorkExperience += 0.1
|
||||
|
||||
if (strategy === 'problem-solving') {
|
||||
const hasTeamLearned = randomFloat(0, 1) > 0.25
|
||||
if (hasTeamLearned) {
|
||||
state.meta.teamWorkExperience += 1.2
|
||||
}
|
||||
} else {
|
||||
state.meta.strategy[strategy]++
|
||||
}
|
||||
|
||||
const [backlog, features] = getFeaturesForNextDay({
|
||||
backlog: state.backlog,
|
||||
features: state.features,
|
||||
steps: state.steps,
|
||||
initialStep: state.steps[0].stepIndex,
|
||||
strategy,
|
||||
teamWorkExperience: state.meta.teamWorkExperience
|
||||
})
|
||||
state.backlog = backlog
|
||||
state.features = features
|
||||
|
||||
const featuresDone = sumElements(state.meta.featuresDonePerDay) ?? 0
|
||||
const featuresDoneNextDay = features.filter(isFeatureDone).length
|
||||
|
||||
state.meta.featuresDonePerDay.push(featuresDoneNextDay - featuresDone)
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
export const isProjectFinished = (features: Feature[]) =>
|
||||
features.every(isFeatureDone)
|
||||
|
||||
export const getMeanComplexity = (features: Feature[]) => {
|
||||
return getMean(features.map((feature) => feature.complexity))
|
||||
}
|
||||
|
||||
export const getMeanLeadTime = (features: Feature[]) => {
|
||||
return getMean(features.map((feature) => feature.leadTime))
|
||||
}
|
||||
|
||||
export const getMeanQualityIssue = (features: Feature[]) => {
|
||||
return getMean(features.map((feature) => feature.qualityIssue))
|
||||
}
|
||||
|
||||
export const simulate = (
|
||||
state: FeatureState,
|
||||
strategy: Strategy
|
||||
): FeatureState => {
|
||||
let i = 0
|
||||
|
||||
while (!isProjectFinished(state.features) && i++ < HARD_STOP) {
|
||||
if (strategy.includes('dps')) {
|
||||
if (state.meta.totalDays % 5 === 0) {
|
||||
state = nextDay(state, 'problem-solving')
|
||||
} else {
|
||||
state = nextDay(state, strategy.split('-')[0] as Strategy)
|
||||
}
|
||||
} else {
|
||||
state = nextDay(state, strategy)
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
36
src/modules/pull-system/feature/feature-steps.ts
Normal file
36
src/modules/pull-system/feature/feature-steps.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
export type FeatureStep = {
|
||||
title: string
|
||||
blueBins: number
|
||||
stepIndex: number
|
||||
}
|
||||
|
||||
const BLUE_BIN_MAX = 2
|
||||
|
||||
const featureSteps: FeatureStep[] = [
|
||||
{
|
||||
title: 'Product',
|
||||
stepIndex: 0,
|
||||
blueBins: BLUE_BIN_MAX
|
||||
},
|
||||
{
|
||||
title: 'Design',
|
||||
stepIndex: 0,
|
||||
blueBins: BLUE_BIN_MAX
|
||||
},
|
||||
{
|
||||
title: 'Development',
|
||||
stepIndex: 0,
|
||||
blueBins: BLUE_BIN_MAX
|
||||
},
|
||||
{
|
||||
title: 'Release',
|
||||
stepIndex: 0,
|
||||
blueBins: 99999
|
||||
}
|
||||
]
|
||||
|
||||
featureSteps.forEach(
|
||||
(feature, index) => (feature.stepIndex = featureSteps.length - index - 1)
|
||||
)
|
||||
|
||||
export { featureSteps }
|
||||
88
src/modules/pull-system/feature/feature-store.ts
Normal file
88
src/modules/pull-system/feature/feature-store.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { Feature } from '@/modules/pull-system/feature/feature'
|
||||
import {
|
||||
getMeanComplexity,
|
||||
getMeanLeadTime,
|
||||
getMeanQualityIssue,
|
||||
initBoard,
|
||||
isFeatureDone,
|
||||
isProjectFinished,
|
||||
newBacklog,
|
||||
nextDay
|
||||
} from '@/modules/pull-system/feature/feature-board'
|
||||
import { featureSteps } from '@/modules/pull-system/feature/feature-steps'
|
||||
import { Strategy } from '@/modules/lean/strategy'
|
||||
import { FeatureState, Meta } from '@/store-type'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
const resetMeta = (): Meta => ({
|
||||
totalDays: 0,
|
||||
teamWorkExperience: 0,
|
||||
featuresDonePerDay: [],
|
||||
strategy: {
|
||||
push: 0,
|
||||
pull: 0,
|
||||
'pull-dps': 0,
|
||||
'push-dps': 0
|
||||
}
|
||||
})
|
||||
|
||||
export const useFeatureStore = defineStore('feature', {
|
||||
state: (): FeatureState => ({
|
||||
steps: [],
|
||||
features: [],
|
||||
backlog: [],
|
||||
meta: resetMeta()
|
||||
}),
|
||||
actions: {
|
||||
async initBoard(limit?: number) {
|
||||
this.backlog = newBacklog(limit)
|
||||
this.steps = featureSteps
|
||||
this.features = initBoard(this.steps, this.backlog)
|
||||
|
||||
this.backlog = this.backlog.filter(
|
||||
(l) => !this.features.find((f) => f.name === l.name)
|
||||
)
|
||||
this.meta = resetMeta()
|
||||
},
|
||||
async nextDay(strategy: Strategy | 'problem-solving') {
|
||||
const newState = nextDay(this.$state, strategy)
|
||||
|
||||
this.backlog = newState.backlog
|
||||
this.meta = newState.meta
|
||||
this.features = newState.features
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
isProjectFinished: (state) => isProjectFinished(state.features),
|
||||
meanComplexity: (state) => getMeanComplexity(state.features),
|
||||
meanLeadTime: (state) => getMeanLeadTime(state.features),
|
||||
meanQualityIssue: (state) => getMeanQualityIssue(state.features),
|
||||
taktTime: (state): string =>
|
||||
(
|
||||
state.meta.totalDays / state.features.filter(isFeatureDone).length || 0
|
||||
).toFixed(2),
|
||||
featuresGroupedByStep: (state) => {
|
||||
const groupedByStep: Record<number, Feature[]> = {}
|
||||
|
||||
state.features.forEach((feature) => {
|
||||
if (!groupedByStep[feature.step]) {
|
||||
groupedByStep[feature.step] = [feature]
|
||||
} else {
|
||||
groupedByStep[feature.step].push(feature)
|
||||
}
|
||||
})
|
||||
|
||||
return groupedByStep
|
||||
},
|
||||
eat(): string {
|
||||
return (
|
||||
parseFloat(this.taktTime) *
|
||||
(this.features.filter((feature) => !isFeatureDone(feature)).length +
|
||||
this.backlog.length)
|
||||
).toFixed(2)
|
||||
},
|
||||
totalFeaturesCount: (state) => state.backlog.length + state.features.length,
|
||||
totalFeaturesDone: (state) =>
|
||||
state.features.filter((feature) => isFeatureDone(feature)).length
|
||||
}
|
||||
})
|
||||
10
src/modules/pull-system/feature/feature.fixture.test.ts
Normal file
10
src/modules/pull-system/feature/feature.fixture.test.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { faker } from '@faker-js/faker'
|
||||
import { describe, it } from 'vitest'
|
||||
|
||||
describe('feature fixture', () => {
|
||||
it('creates lots of animals', () => {
|
||||
for (let i = 0; i < 200; i++) {
|
||||
console.log(`"${faker.animal.bird()}", `)
|
||||
}
|
||||
})
|
||||
})
|
||||
12
src/modules/pull-system/feature/feature.fixture.ts
Normal file
12
src/modules/pull-system/feature/feature.fixture.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { birds } from '@/data/bird'
|
||||
import { Feature } from '@/modules/pull-system/feature/feature'
|
||||
import { randomInteger } from '@/utils'
|
||||
|
||||
export const features: Feature[] = birds.map((name) => ({
|
||||
name,
|
||||
complexity: randomInteger(1, 5),
|
||||
leadTime: 0,
|
||||
status: 'doing',
|
||||
step: Infinity,
|
||||
qualityIssue: 0
|
||||
}))
|
||||
10
src/modules/pull-system/feature/feature.ts
Normal file
10
src/modules/pull-system/feature/feature.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export type FeatureStatus = 'doing' | 'done'
|
||||
|
||||
export type Feature = {
|
||||
name: string
|
||||
complexity: number
|
||||
leadTime: number
|
||||
status: FeatureStatus
|
||||
step: number
|
||||
qualityIssue: number
|
||||
}
|
||||
1
src/modules/pull-system/lean/strategy.ts
Normal file
1
src/modules/pull-system/lean/strategy.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type Strategy = 'push' | 'pull' | 'pull-dps' | 'push-dps'
|
||||
84
src/modules/pull-system/simulation/SimulationChart.vue
Normal file
84
src/modules/pull-system/simulation/SimulationChart.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import { useSimulationStore } from '@/modules/pull-system/simulation/simulation-store'
|
||||
import { popNElement } from '@/utils'
|
||||
import chartXkcd from 'chart.xkcd'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
|
||||
const simulationStore = useSimulationStore()
|
||||
const SAMPLE = 15
|
||||
const svgElement = ref<HTMLInputElement | null>(null)
|
||||
const isChartInit = ref(false)
|
||||
|
||||
const reloadGraph = () => {
|
||||
if (simulationStore.dashboards.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
isChartInit.value = true
|
||||
|
||||
if (!svgElement.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const samples = popNElement(
|
||||
[...simulationStore.dashboards],
|
||||
SAMPLE
|
||||
).toReversed()
|
||||
|
||||
const config = {
|
||||
title: `${SAMPLE} simulations`,
|
||||
xLabel: 'days',
|
||||
yLabel: 'features done',
|
||||
data: {
|
||||
labels: Array.from(
|
||||
{
|
||||
length: Math.max(
|
||||
...samples.map((dashboard) => dashboard.meta.totalDays)
|
||||
)
|
||||
},
|
||||
(_, index) => index + 1
|
||||
),
|
||||
datasets: samples.map((dashboard, index) => ({
|
||||
label: `${dashboard.analysis.strategy} ${index + 1}`,
|
||||
data: dashboard.meta.featuresDonePerDay
|
||||
}))
|
||||
},
|
||||
options: {
|
||||
showLegend: false,
|
||||
fontFamily: 'Noto Serif'
|
||||
}
|
||||
}
|
||||
new chartXkcd.Line(svgElement.value, config)
|
||||
}
|
||||
|
||||
watch(() => simulationStore.hasSimulationFinished, reloadGraph)
|
||||
watch(svgElement, reloadGraph)
|
||||
onMounted(reloadGraph)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="simulation-chart">
|
||||
<div v-if="!isChartInit" class="chart no-init">
|
||||
Chart appears once every simulations resume
|
||||
</div>
|
||||
<svg ref="svgElement" v-else class="chart"></svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.simulation-chart {
|
||||
--chart-height: 80vh;
|
||||
height: var(--chart-height);
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 80vw;
|
||||
height: var(--chart-height);
|
||||
}
|
||||
|
||||
.no-init {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
61
src/modules/pull-system/simulation/SimulationControls.vue
Normal file
61
src/modules/pull-system/simulation/SimulationControls.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<script setup lang="ts">
|
||||
import { Strategy } from '@/modules/lean/strategy'
|
||||
import { useSimulationStore } from '@/modules/pull-system/simulation/simulation-store'
|
||||
|
||||
defineProps<{
|
||||
type: 'single' | 'multiple'
|
||||
}>()
|
||||
|
||||
const simulationStore = useSimulationStore()
|
||||
const NUMBER_OF_SIMULATION = 200
|
||||
|
||||
const strategies: Strategy[] = ['push', 'pull', 'push-dps', 'pull-dps']
|
||||
|
||||
const simulateEverything = () => {
|
||||
strategies.forEach((strategy) =>
|
||||
simulationStore.multiSimulation(NUMBER_OF_SIMULATION, strategy)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="simulation-controls">
|
||||
<div class="row" v-if="type === 'single'">
|
||||
<button
|
||||
class="button button-outline"
|
||||
@click="simulationStore.multiSimulation(1, 'push')"
|
||||
>
|
||||
simulate push system
|
||||
</button>
|
||||
<button
|
||||
class="button button-outline"
|
||||
@click="simulationStore.multiSimulation(1, 'pull')"
|
||||
>
|
||||
simulate pull system
|
||||
</button>
|
||||
<button
|
||||
class="button button-outline"
|
||||
@click="simulationStore.multiSimulation(1, 'push-dps')"
|
||||
>
|
||||
simulate push with DPS
|
||||
</button>
|
||||
<button
|
||||
class="button button-outline"
|
||||
@click="simulationStore.multiSimulation(1, 'pull-dps')"
|
||||
>
|
||||
simulate pull with DPS
|
||||
</button>
|
||||
</div>
|
||||
<div class="row" v-else-if="type === 'multiple'">
|
||||
<button class="button button-outline" @click="simulateEverything">
|
||||
simulate {{ NUMBER_OF_SIMULATION }} simulations for each system
|
||||
</button>
|
||||
<button
|
||||
class="button button-clear"
|
||||
@click="simulationStore.clearDashboard()"
|
||||
>
|
||||
clear dashboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
83
src/modules/pull-system/simulation/SimulationDashboard.vue
Normal file
83
src/modules/pull-system/simulation/SimulationDashboard.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { Strategy } from '@/modules/lean/strategy'
|
||||
import { useSimulationStore } from '@/modules/pull-system/simulation/simulation-store'
|
||||
|
||||
const simulationStore = useSimulationStore()
|
||||
|
||||
const strategies: Strategy[] = ['push', 'pull', 'push-dps', 'pull-dps']
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="simulation-dashboard">
|
||||
<h3>Dashboard</h3>
|
||||
<h4>
|
||||
{{ simulationStore.simulationsDone
|
||||
}}<span class="sub">/{{ simulationStore.requestedSimulation }}</span>
|
||||
simulations
|
||||
</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>mean values</th>
|
||||
<th class="numeric">push</th>
|
||||
<th class="numeric">pull</th>
|
||||
<th class="numeric">push with problem solving</th>
|
||||
<th class="numeric">pull with problem solving</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Total days</td>
|
||||
<td class="numeric" v-for="strategy in strategies" :key="strategy">
|
||||
{{ simulationStore.meanTotalDays(strategy) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Lead time</td>
|
||||
<td class="numeric" v-for="strategy in strategies" :key="strategy">
|
||||
{{ simulationStore.meanLeadTime(strategy) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Takt time</td>
|
||||
<td class="numeric" v-for="strategy in strategies" :key="strategy">
|
||||
{{ simulationStore.meanTaktTime(strategy) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Complexity</td>
|
||||
<td class="numeric" v-for="strategy in strategies" :key="strategy">
|
||||
{{ simulationStore.meanComplexity(strategy) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Quality issue</td>
|
||||
<td class="numeric" v-for="strategy in strategies" :key="strategy">
|
||||
{{ simulationStore.meanQuality(strategy) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Team work exp.</td>
|
||||
<td class="numeric" v-for="strategy in strategies" :key="strategy">
|
||||
{{ simulationStore.meanTeamWorkExperience(strategy) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.simulation-dashboard {
|
||||
color: var(--primary-color);
|
||||
width: 100%;
|
||||
|
||||
table {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.numeric {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
161
src/modules/pull-system/simulation/simulation-store.ts
Normal file
161
src/modules/pull-system/simulation/simulation-store.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { featureSteps } from '@/modules/pull-system/feature/feature-steps'
|
||||
import { Strategy } from '@/modules/lean/strategy'
|
||||
import { Dashboard, Meta } from '@/store-type'
|
||||
import { getRound } from '@/utils'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
type Mean = {
|
||||
leadTimeSum: number
|
||||
taktTimeSum: number
|
||||
complexitySum: number
|
||||
qualityIssueSum: number
|
||||
teamWorkExperienceSum: number
|
||||
totalDaysSum: number
|
||||
simulations: number
|
||||
}
|
||||
|
||||
type State = {
|
||||
dashboards: Dashboard[]
|
||||
requestedSimulation: number
|
||||
simulationsDone: number
|
||||
mean: Record<Strategy, Mean>
|
||||
}
|
||||
|
||||
const newMean = (): Mean => ({
|
||||
leadTimeSum: 0,
|
||||
taktTimeSum: 0,
|
||||
complexitySum: 0,
|
||||
qualityIssueSum: 0,
|
||||
teamWorkExperienceSum: 0,
|
||||
totalDaysSum: 0,
|
||||
simulations: 0
|
||||
})
|
||||
|
||||
const instance = new ComlinkWorker<typeof import('../feature/feature-board')>(
|
||||
new URL('../feature/feature-board', import.meta.url)
|
||||
)
|
||||
|
||||
const resetMeta = (): Meta => ({
|
||||
totalDays: 0,
|
||||
teamWorkExperience: 0,
|
||||
featuresDonePerDay: [],
|
||||
strategy: {
|
||||
push: 0,
|
||||
pull: 0,
|
||||
'pull-dps': 0,
|
||||
'push-dps': 0
|
||||
}
|
||||
})
|
||||
|
||||
export const useSimulationStore = defineStore('dashboard', {
|
||||
state: (): State => {
|
||||
return {
|
||||
dashboards: [],
|
||||
requestedSimulation: 0,
|
||||
simulationsDone: 0,
|
||||
mean: {
|
||||
push: newMean(),
|
||||
pull: newMean(),
|
||||
'pull-dps': newMean(),
|
||||
'push-dps': newMean()
|
||||
}
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
async simulate(strategy: Strategy) {
|
||||
const steps = featureSteps
|
||||
const backlog = await instance.newBacklog()
|
||||
const features = await instance.initBoard(steps, backlog)
|
||||
|
||||
const newState = await instance.simulate(
|
||||
{
|
||||
backlog,
|
||||
steps,
|
||||
features,
|
||||
meta: resetMeta()
|
||||
},
|
||||
strategy
|
||||
)
|
||||
|
||||
const [worstFeature] = newState.features.sort((a, b) =>
|
||||
a.qualityIssue > b.qualityIssue ? -1 : 1
|
||||
)
|
||||
|
||||
const dashboard: Dashboard = {
|
||||
uuid: new Date().getTime().toString(),
|
||||
meta: newState.meta,
|
||||
analysis: {
|
||||
meanComplexity: await instance.getMeanComplexity(newState.features),
|
||||
meanLeadTime: await instance.getMeanLeadTime(newState.features),
|
||||
meanQualityIssue: await instance.getMeanQualityIssue(
|
||||
newState.features
|
||||
),
|
||||
worstFeature,
|
||||
strategy
|
||||
}
|
||||
}
|
||||
|
||||
this.dashboards.push(dashboard)
|
||||
this.mean[strategy].leadTimeSum += dashboard.analysis.meanLeadTime
|
||||
this.mean[strategy].taktTimeSum +=
|
||||
dashboard.meta.totalDays / newState.features.length
|
||||
this.mean[strategy].complexitySum += dashboard.analysis.meanComplexity
|
||||
this.mean[strategy].qualityIssueSum += dashboard.analysis.meanQualityIssue
|
||||
this.mean[strategy].teamWorkExperienceSum +=
|
||||
dashboard.meta.teamWorkExperience
|
||||
this.mean[strategy].totalDaysSum += dashboard.meta.totalDays
|
||||
this.mean[strategy].simulations++
|
||||
},
|
||||
async multiSimulation(simulations: number, strategy: Strategy) {
|
||||
this.requestedSimulation += simulations
|
||||
for (let i = 0; i < simulations; i++) {
|
||||
await this.simulate(strategy)
|
||||
this.simulationsDone++
|
||||
}
|
||||
},
|
||||
clearDashboard() {
|
||||
this.dashboards = []
|
||||
this.mean.push = newMean()
|
||||
this.mean.pull = newMean()
|
||||
this.mean['pull-dps'] = newMean()
|
||||
this.mean['push-dps'] = newMean()
|
||||
this.simulationsDone = 0
|
||||
this.requestedSimulation = 0
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
meanLeadTime: (state) => (strategy: Strategy) =>
|
||||
getRound(
|
||||
state.mean[strategy].leadTimeSum,
|
||||
state.mean[strategy].simulations
|
||||
),
|
||||
meanTaktTime: (state) => (strategy: Strategy) =>
|
||||
getRound(
|
||||
state.mean[strategy].taktTimeSum,
|
||||
state.mean[strategy].simulations
|
||||
),
|
||||
meanComplexity: (state) => (strategy: Strategy) =>
|
||||
getRound(
|
||||
state.mean[strategy].complexitySum,
|
||||
state.mean[strategy].simulations
|
||||
),
|
||||
meanQuality: (state) => (strategy: Strategy) =>
|
||||
getRound(
|
||||
state.mean[strategy].qualityIssueSum,
|
||||
state.mean[strategy].simulations
|
||||
),
|
||||
meanTeamWorkExperience: (state) => (strategy: Strategy) =>
|
||||
getRound(
|
||||
state.mean[strategy].teamWorkExperienceSum,
|
||||
state.mean[strategy].simulations
|
||||
),
|
||||
meanTotalDays: (state) => (strategy: Strategy) =>
|
||||
getRound(
|
||||
state.mean[strategy].totalDaysSum,
|
||||
state.mean[strategy].simulations
|
||||
),
|
||||
hasSimulationFinished: (state) =>
|
||||
state.requestedSimulation > 0 &&
|
||||
state.requestedSimulation === state.simulationsDone
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user